Содержание

  • 1  Цель и задачи работы
  • 2  Первоначальное ознакомление с данными
  • 3  Объединение данных
    • 3.1  Изменение названия столбцов
  • 4  Выделение целевого признака
  • 5  Исследование данных
    • 5.1  Предварительный аналих данных
    • 5.2  Изменение типа данных
      • 5.2.1  senior_citizen
      • 5.2.2  total_charges
      • 5.2.3  begin_date, end_date
    • 5.3  Заполнение пропусков
      • 5.3.1  total_charges
      • 5.3.2  end_date
      • 5.3.3  multiple_lines
      • 5.3.4  internet_service, online_security, online_backup, device_protection, tech_support, streaming_tv, streaming_movies
    • 5.4  Анализ признаков с количественными значениями
    • 5.5  Анализ признаков с категориальными значениями
  • 6  Удаление и создание признаков
    • 6.1  Создание признаков
    • 6.2  Анализ синтетических признаков
    • 6.3  Удаление избыточных признаков
  • 7  Корреляционный анализ
    • 7.1  Проверка корреляции признаков
    • 7.2  Удаление признаков c низкой корреляцией и мультиколлиниарностью
  • 8  Выводы по 1 разделу
  • 9  Разбиение датасета на обучающую и тестовую выборки
  • 10  Кодирование и масштабирование признаков
  • 11  Выбор и обучение моделей машинного обучения на кросс-валидации
    • 11.1  LogisticRegression
    • 11.2  RandomForestClassifier
    • 11.3  LGBMClassifier
    • 11.4  CatBoostClassifier
  • 12  Выбор лучшей модели по метрике ROC-AUC на кросс-валидации
  • 13  Оценка качества лучшей модели на тестовой выборке
  • 14  Построение и анализ ROC-кривой, accuracy, матрицы ошибок и важности признаков лучшей модели на тестовой выборке.
    • 14.1  ROC-кривая
    • 14.2  Accuracy
    • 14.3  Матрица ошибок
    • 14.4  Важность признаков
  • 15  Выводы по 2 разделу
  • 16  Отчет о проделанной работе
    • 16.1  Выполнение плана
    • 16.2  Трудности, возникшие при выполнении работы:
    • 16.3  Ключевые шаги в решении задачи
      • 16.3.1  Подготовка данных
      • 16.3.2  Обучение моделей машинного обучения
      • 16.3.3  Проверка качества выбранной модели
      • 16.3.4  Рекомендации о введении модели в эксплуатацию (или её переработки)

Модель машинного обучения, предсказывающая уход клиентов оператора связи «Ниединогоразрыва.ком»¶

Оператору связи «Ниединогоразрыва.ком» необходимо предсказывать отток клиентов. Если согласно предсказанию, пользователь планирует уйти, ему должны быть предложены промокоды и специальные условия. Для предсказания требуется разработать качественную модель машинного обучения с метрикой ROC-AUC на тестовой выборке в 0.85. Помимо ROC-AUC необходимо предоставить интерпретируемую метрику, такие как accuracy и матрица ошибок.

Заказчик предоставил следующие данные:

  • personal_new.csv - персональные данные клиента
    • gender - пол
    • SeniorCitizen - наличие пенсионного статуса по возрасту
    • Partner - наличие супруга/супруги
    • Dependents - наличие иждивенцев
  • contract_new.csv - информация о договоре
    • BeginDate - дата начала пользования услугами
    • EndDate - дата окончания пользования услугами
    • Type - тип договора: ежемесячный, годовой и т.д
    • PaperlessBilling - выставления счёта по электронной почте
    • PaymentMethod - способ оплаты
    • MonthlyCharges - ежемесячные траты на услуги
    • TotalCharges - всего потрачено денег на услуги
  • internet_new.csv - информация об интернет-услугах
    • InternetService - наличие услуг Интернет
    • OnlineSecurity - межсетевой экран
    • OnlineBackup - облачное хранилище файлов для резервного копирования данных
    • DeviceProtection - антивирус
    • TechSupport - выделенная линия технической поддержки
    • StreamingTV - онлайн-ТВ
    • StreamingMovies - онлайн-кинотеатр
  • phone_new.csv - информация об услугах телефонии
    • MultipleLines - возможность подключения телефонного аппарата к нескольким линиям одновременно

План работы¶

Цель и задачи работы¶

Цель работы - построить модель машинного обучения, предсказывающая уход клиентов оператора, при чем метрика ROC-AUC на тестовой выборке должна составлять не менее 0.85.

Задачи:

  • Первоначальное ознакомление с данными
  • Объединение данных
  • Выделение целевого признака
  • Исследование и обработка данных
  • Удаление и создание признаков
  • Корреляционный анализ
  • Разбиение датасета на обучающую и тестовую выборки
  • Кодирование и масштабирование признаков
  • Выбор и обучение разных моделей машинного обучения на кросс-валидации
  • Выбор лучшей модели по метрике ROC-AUC на кросс-валидации
  • Оценка качества лучшей модели на тестовой выборке
  • Построение и анализ ROC-кривой, accuracy, матрицы ошибок и важности признаков лучшей модели на тестовой выборке.
  • Отчет о проделанной работе
In [1]:
# Загрузка библиотек, необходимых в проекте:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import re

from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import mean_absolute_error, roc_auc_score, accuracy_score, confusion_matrix
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import FunctionTransformer, OneHotEncoder, OrdinalEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, RandomizedSearchCV, GridSearchCV, train_test_split
from sklearn.metrics import roc_curve 
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.compose import ColumnTransformer


!pip install phik
import phik
from phik import resources
from phik.binning import bin_data
from phik.report import plot_correlation_matrix


!pip install catboost
import catboost as cat
from catboost import CatBoostClassifier, Pool, cv

import warnings
warnings.filterwarnings("ignore")

!pip install lightgbm
import lightgbm
from lightgbm import LGBMClassifier
Requirement already satisfied: phik in c:\users\user\anaconda3\lib\site-packages (0.12.3)
Requirement already satisfied: numpy>=1.18.0 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.24.3)
Requirement already satisfied: scipy>=1.5.2 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.10.1)
Requirement already satisfied: pandas>=0.25.1 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.5.3)
Requirement already satisfied: matplotlib>=2.2.3 in c:\users\user\anaconda3\lib\site-packages (from phik) (3.7.1)
Requirement already satisfied: joblib>=0.14.1 in c:\users\user\anaconda3\lib\site-packages (from phik) (1.2.0)
Requirement already satisfied: contourpy>=1.0.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (1.0.5)
Requirement already satisfied: cycler>=0.10 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (0.11.0)
Requirement already satisfied: fonttools>=4.22.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (4.25.0)
Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (1.4.4)
Requirement already satisfied: packaging>=20.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (23.0)
Requirement already satisfied: pillow>=6.2.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (9.4.0)
Requirement already satisfied: pyparsing>=2.3.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (3.0.9)
Requirement already satisfied: python-dateutil>=2.7 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (2.8.2)
Requirement already satisfied: importlib-resources>=3.2.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib>=2.2.3->phik) (5.2.0)
Requirement already satisfied: pytz>=2020.1 in c:\users\user\anaconda3\lib\site-packages (from pandas>=0.25.1->phik) (2022.7)
Requirement already satisfied: zipp>=3.1.0 in c:\users\user\anaconda3\lib\site-packages (from importlib-resources>=3.2.0->matplotlib>=2.2.3->phik) (3.11.0)
Requirement already satisfied: six>=1.5 in c:\users\user\anaconda3\lib\site-packages (from python-dateutil>=2.7->matplotlib>=2.2.3->phik) (1.16.0)
Requirement already satisfied: catboost in c:\users\user\anaconda3\lib\site-packages (1.2)
Requirement already satisfied: graphviz in c:\users\user\anaconda3\lib\site-packages (from catboost) (0.20.1)
Requirement already satisfied: matplotlib in c:\users\user\anaconda3\lib\site-packages (from catboost) (3.7.1)
Requirement already satisfied: numpy>=1.16.0 in c:\users\user\anaconda3\lib\site-packages (from catboost) (1.24.3)
Requirement already satisfied: pandas>=0.24 in c:\users\user\anaconda3\lib\site-packages (from catboost) (1.5.3)
Requirement already satisfied: scipy in c:\users\user\anaconda3\lib\site-packages (from catboost) (1.10.1)
Requirement already satisfied: plotly in c:\users\user\anaconda3\lib\site-packages (from catboost) (5.9.0)
Requirement already satisfied: six in c:\users\user\anaconda3\lib\site-packages (from catboost) (1.16.0)
Requirement already satisfied: python-dateutil>=2.8.1 in c:\users\user\anaconda3\lib\site-packages (from pandas>=0.24->catboost) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in c:\users\user\anaconda3\lib\site-packages (from pandas>=0.24->catboost) (2022.7)
Requirement already satisfied: contourpy>=1.0.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (1.0.5)
Requirement already satisfied: cycler>=0.10 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (0.11.0)
Requirement already satisfied: fonttools>=4.22.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (4.25.0)
Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (1.4.4)
Requirement already satisfied: packaging>=20.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (23.0)
Requirement already satisfied: pillow>=6.2.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (9.4.0)
Requirement already satisfied: pyparsing>=2.3.1 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (3.0.9)
Requirement already satisfied: importlib-resources>=3.2.0 in c:\users\user\anaconda3\lib\site-packages (from matplotlib->catboost) (5.2.0)
Requirement already satisfied: tenacity>=6.2.0 in c:\users\user\anaconda3\lib\site-packages (from plotly->catboost) (8.2.2)
Requirement already satisfied: zipp>=3.1.0 in c:\users\user\anaconda3\lib\site-packages (from importlib-resources>=3.2.0->matplotlib->catboost) (3.11.0)
Requirement already satisfied: lightgbm in c:\users\user\anaconda3\lib\site-packages (3.3.5)
Requirement already satisfied: wheel in c:\users\user\anaconda3\lib\site-packages (from lightgbm) (0.38.4)
Requirement already satisfied: numpy in c:\users\user\anaconda3\lib\site-packages (from lightgbm) (1.24.3)
Requirement already satisfied: scipy in c:\users\user\anaconda3\lib\site-packages (from lightgbm) (1.10.1)
Requirement already satisfied: scikit-learn!=0.22.0 in c:\users\user\anaconda3\lib\site-packages (from lightgbm) (1.3.0)
Requirement already satisfied: joblib>=1.1.1 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn!=0.22.0->lightgbm) (1.2.0)
Requirement already satisfied: threadpoolctl>=2.0.0 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn!=0.22.0->lightgbm) (2.2.0)
In [2]:
!pip install -U scikit-learn
Requirement already satisfied: scikit-learn in c:\users\user\anaconda3\lib\site-packages (1.3.0)
Requirement already satisfied: numpy>=1.17.3 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn) (1.24.3)
Requirement already satisfied: scipy>=1.5.0 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn) (1.10.1)
Requirement already satisfied: joblib>=1.1.1 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn) (1.2.0)
Requirement already satisfied: threadpoolctl>=2.0.0 in c:\users\user\anaconda3\lib\site-packages (from scikit-learn) (2.2.0)

Первоначальное ознакомление с данными¶

In [3]:
# Загрузим данные из файла:

# contract_new
try:
    contract_new = pd.read_csv('') 
except:
    try:
        contract_new = pd.read_csv('D:/YandexDisk/Яндекс практикум/18 Финальный проект/contract_new.csv') # локально Windiws work
    except:
        contract_new = pd.read_csv('/datasets/contract_new.csv') # онлайн
contract_new.name = 'Информация о договоре'  
        
# personal_new
try:
    personal_new = pd.read_csv('') # онлайн
except:
    try:
        personal_new = pd.read_csv('D:/YandexDisk/Яндекс практикум/18 Финальный проект/personal_new.csv') # локально Windiws work
    except:
        personal_new = pd.read_csv('/datasets/personal_new.csv') # онлайн
personal_new.name = 'Персональные данные клиента' 

        
# internet_new
try:
    internet_new = pd.read_csv('') # онлайн
except:
    try:
        internet_new = pd.read_csv('D:/YandexDisk/Яндекс практикум/18 Финальный проект/internet_new.csv') # локально Windiws work
    except:
        internet_new = pd.read_csv('/datasets/internet_new.csv') # онлайн
internet_new.name = 'Информация об интернет-услугах' 
                         

# phone_new
try:
    phone_new = pd.read_csv('https://code.s3.yandex.net/datasets/phone_new.csv') # онлайн
except:
    try:
        phone_new = pd.read_csv('D:/YandexDisk/Яндекс практикум/18 Финальный проект/phone_new.csv') # локально Windiws work
    except:
        phone_new = pd.read_csv('/datasets/phone_new.csv') # онлайн
phone_new.name = 'Информация об услугах телефонии' 
        

Изучим таблицы. Для этого напишем функцию для вывода информации о датафреймах.

In [4]:
def data_analysis (data):
    print ('\033[1m' + data.name +'\033[0m')
    display(data.head()),
    print()
    print('Количество строк:', data.shape[0])
    print('Количество столбцов:', data.shape[1])
    print()
    print('Информация о таблице:')
    data.info()
    print()
    print()
In [5]:
data_list = [contract_new, personal_new, internet_new, phone_new]
for i in data_list:
    data_analysis(i)
Информация о договоре
customerID BeginDate EndDate Type PaperlessBilling PaymentMethod MonthlyCharges TotalCharges
0 7590-VHVEG 2020-01-01 No Month-to-month Yes Electronic check 29.85 31.04
1 5575-GNVDE 2017-04-01 No One year No Mailed check 56.95 2071.84
2 3668-QPYBK 2019-10-01 No Month-to-month Yes Mailed check 53.85 226.17
3 7795-CFOCW 2016-05-01 No One year No Bank transfer (automatic) 42.30 1960.6
4 9237-HQITU 2019-09-01 No Month-to-month Yes Electronic check 70.70 353.5
Количество строк: 7043
Количество столбцов: 8

Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 8 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   BeginDate         7043 non-null   object 
 2   EndDate           7043 non-null   object 
 3   Type              7043 non-null   object 
 4   PaperlessBilling  7043 non-null   object 
 5   PaymentMethod     7043 non-null   object 
 6   MonthlyCharges    7043 non-null   float64
 7   TotalCharges      7043 non-null   object 
dtypes: float64(1), object(7)
memory usage: 440.3+ KB


Персональные данные клиента
customerID gender SeniorCitizen Partner Dependents
0 7590-VHVEG Female 0 Yes No
1 5575-GNVDE Male 0 No No
2 3668-QPYBK Male 0 No No
3 7795-CFOCW Male 0 No No
4 9237-HQITU Female 0 No No
Количество строк: 7043
Количество столбцов: 5

Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 5 columns):
 #   Column         Non-Null Count  Dtype 
---  ------         --------------  ----- 
 0   customerID     7043 non-null   object
 1   gender         7043 non-null   object
 2   SeniorCitizen  7043 non-null   int64 
 3   Partner        7043 non-null   object
 4   Dependents     7043 non-null   object
dtypes: int64(1), object(4)
memory usage: 275.2+ KB


Информация об интернет-услугах
customerID InternetService OnlineSecurity OnlineBackup DeviceProtection TechSupport StreamingTV StreamingMovies
0 7590-VHVEG DSL No Yes No No No No
1 5575-GNVDE DSL Yes No Yes No No No
2 3668-QPYBK DSL Yes Yes No No No No
3 7795-CFOCW DSL Yes No Yes Yes No No
4 9237-HQITU Fiber optic No No No No No No
Количество строк: 5517
Количество столбцов: 8

Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5517 entries, 0 to 5516
Data columns (total 8 columns):
 #   Column            Non-Null Count  Dtype 
---  ------            --------------  ----- 
 0   customerID        5517 non-null   object
 1   InternetService   5517 non-null   object
 2   OnlineSecurity    5517 non-null   object
 3   OnlineBackup      5517 non-null   object
 4   DeviceProtection  5517 non-null   object
 5   TechSupport       5517 non-null   object
 6   StreamingTV       5517 non-null   object
 7   StreamingMovies   5517 non-null   object
dtypes: object(8)
memory usage: 344.9+ KB


Информация об услугах телефонии
customerID MultipleLines
0 5575-GNVDE No
1 3668-QPYBK No
2 9237-HQITU No
3 9305-CDSKC Yes
4 1452-KIOVK Yes
Количество строк: 6361
Количество столбцов: 2

Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6361 entries, 0 to 6360
Data columns (total 2 columns):
 #   Column         Non-Null Count  Dtype 
---  ------         --------------  ----- 
 0   customerID     6361 non-null   object
 1   MultipleLines  6361 non-null   object
dtypes: object(2)
memory usage: 99.5+ KB


Информация в датафреймах представлена количественными и категориальными значениями, встречаются типы данных: float64, object, int64

Датафрейм с информацией о договоре содержит 7043 строк и 8 колонок, датафрейм с информацией о персональных данных клиента - 7043 строк и 5 колонок, датафрейм с информацией об интернет-услугах - 5517 строк и 8 колонок, датафрейм с информацией об услугах телефонии - 6361 строк и 2 колонки.

Название столбцов не соответствует стилю кода языка Python согласно руководству PEP8.

Объединение данных¶

Объединим данные перед проведением детальным анализом данных:

In [6]:
df = contract_new.merge(
    personal_new, on='customerID', how='outer'
).merge(
    internet_new, on='customerID', how='outer'
).merge(
    phone_new, on='customerID', how='outer'
)
df.name = 'Объединенная таблица'  
In [7]:
print('Количество строк:', df.shape[0])
print('Количество столбцов:', df.shape[1])
print('Название столбцов:', df.columns.values)
Количество строк: 7043
Количество столбцов: 20
Название столбцов: ['customerID' 'BeginDate' 'EndDate' 'Type' 'PaperlessBilling'
 'PaymentMethod' 'MonthlyCharges' 'TotalCharges' 'gender' 'SeniorCitizen'
 'Partner' 'Dependents' 'InternetService' 'OnlineSecurity' 'OnlineBackup'
 'DeviceProtection' 'TechSupport' 'StreamingTV' 'StreamingMovies'
 'MultipleLines']

Объединенная таблица df содержит 7043 строк и 20 колонок, объединение прошло успешно.

In [8]:
# код тимлида для проверки
data_analysis(df)
Объединенная таблица
customerID BeginDate EndDate Type PaperlessBilling PaymentMethod MonthlyCharges TotalCharges gender SeniorCitizen Partner Dependents InternetService OnlineSecurity OnlineBackup DeviceProtection TechSupport StreamingTV StreamingMovies MultipleLines
0 7590-VHVEG 2020-01-01 No Month-to-month Yes Electronic check 29.85 31.04 Female 0 Yes No DSL No Yes No No No No NaN
1 5575-GNVDE 2017-04-01 No One year No Mailed check 56.95 2071.84 Male 0 No No DSL Yes No Yes No No No No
2 3668-QPYBK 2019-10-01 No Month-to-month Yes Mailed check 53.85 226.17 Male 0 No No DSL Yes Yes No No No No No
3 7795-CFOCW 2016-05-01 No One year No Bank transfer (automatic) 42.30 1960.6 Male 0 No No DSL Yes No Yes Yes No No NaN
4 9237-HQITU 2019-09-01 No Month-to-month Yes Electronic check 70.70 353.5 Female 0 No No Fiber optic No No No No No No No
Количество строк: 7043
Количество столбцов: 20

Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 7043 entries, 0 to 7042
Data columns (total 20 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   BeginDate         7043 non-null   object 
 2   EndDate           7043 non-null   object 
 3   Type              7043 non-null   object 
 4   PaperlessBilling  7043 non-null   object 
 5   PaymentMethod     7043 non-null   object 
 6   MonthlyCharges    7043 non-null   float64
 7   TotalCharges      7043 non-null   object 
 8   gender            7043 non-null   object 
 9   SeniorCitizen     7043 non-null   int64  
 10  Partner           7043 non-null   object 
 11  Dependents        7043 non-null   object 
 12  InternetService   5517 non-null   object 
 13  OnlineSecurity    5517 non-null   object 
 14  OnlineBackup      5517 non-null   object 
 15  DeviceProtection  5517 non-null   object 
 16  TechSupport       5517 non-null   object 
 17  StreamingTV       5517 non-null   object 
 18  StreamingMovies   5517 non-null   object 
 19  MultipleLines     6361 non-null   object 
dtypes: float64(1), int64(1), object(18)
memory usage: 1.1+ MB


Изменение названия столбцов¶

Приведем названия столбцов согласно правильного стиля PEP8 , для этого построим функцию:

In [9]:
def convert_to_snake(columns):
    return columns.str.replace('(?<=[a-z])(?=[A-Z])', '_', regex=True).str.lower()
In [10]:
df.columns = convert_to_snake(df.columns)
df.columns
Out[10]:
Index(['customer_id', 'begin_date', 'end_date', 'type', 'paperless_billing',
       'payment_method', 'monthly_charges', 'total_charges', 'gender',
       'senior_citizen', 'partner', 'dependents', 'internet_service',
       'online_security', 'online_backup', 'device_protection', 'tech_support',
       'streaming_tv', 'streaming_movies', 'multiple_lines'],
      dtype='object')

Выделение целевого признака¶

Выделим целевой признак. Рассмотрим подробно признак "Дата окончания пользования услугами".

In [11]:
df['end_date'].unique()
Out[11]:
array(['No', '2017-05-01', '2016-03-01', '2018-09-01', '2018-11-01',
       '2018-12-01', '2019-08-01', '2018-07-01', '2017-09-01',
       '2015-09-01', '2016-07-01', '2016-06-01', '2018-03-01',
       '2019-02-01', '2018-06-01', '2019-06-01', '2020-01-01',
       '2019-11-01', '2016-09-01', '2015-06-01', '2016-12-01',
       '2019-05-01', '2019-04-01', '2017-06-01', '2017-08-01',
       '2018-04-01', '2018-08-01', '2018-02-01', '2019-07-01',
       '2015-12-01', '2014-06-01', '2018-10-01', '2019-01-01',
       '2017-07-01', '2017-12-01', '2018-05-01', '2015-11-01',
       '2019-10-01', '2019-03-01', '2016-02-01', '2016-10-01',
       '2018-01-01', '2017-11-01', '2015-10-01', '2019-12-01',
       '2015-07-01', '2017-04-01', '2015-02-01', '2017-03-01',
       '2016-05-01', '2016-11-01', '2015-08-01', '2019-09-01',
       '2017-10-01', '2017-02-01', '2016-08-01', '2016-04-01',
       '2015-05-01', '2014-09-01', '2014-10-01', '2017-01-01',
       '2015-03-01', '2015-01-01', '2016-01-01', '2015-04-01',
       '2014-12-01', '2014-11-01'], dtype=object)

Рассматривая данный признак можно выделить, что факт ухода клиента оператора связи «Ниединогоразрыва.ком» подтверждается наличем даты уходы. Если факт ухода не зафиксирован, значением колонки 'EndDate' будет 'No'. Создадим целевой признак, при котором значение 1 будет соответсовать факту ухода клиента, 0 - факту, что клиент продолжается пользоваться услугами оператора.

In [12]:
df['leave'] = (
    df['end_date'].where(
        df['end_date'] == 'No', 'Yes')
)
In [13]:
display(df[df['end_date'] != 'No'][['begin_date','end_date','leave']].head())
df[df['end_date'] == 'No'][['begin_date','end_date','leave']].head()
begin_date end_date leave
9 2014-12-01 2017-05-01 Yes
15 2014-05-01 2016-03-01 Yes
25 2017-08-01 2018-09-01 Yes
30 2014-03-01 2018-11-01 Yes
35 2014-02-01 2018-12-01 Yes
Out[13]:
begin_date end_date leave
0 2020-01-01 No No
1 2017-04-01 No No
2 2019-10-01 No No
3 2016-05-01 No No
4 2019-09-01 No No

Корректность созданнного признака подтверждена. Проверим дисбаланс классов:

In [14]:
print ('Процентное соотношение значений столбца "leave":\n', 
       round(df['leave'].value_counts()/df['leave'].count()*100))
print ()

(df['leave'].value_counts()/df['leave'].count()*100).plot(
    kind='bar', 
    grid = True
).set(
    ylabel = 'Относительное количество, %', 
    xlabel = 'Факт ухода клиента: \n "No" - Клиент не ушел, "Yes" - Клиент ушел', 
    title = 'Распределение оставшихся и ушедщих клиентов' 
)
plt.show()
Процентное соотношение значений столбца "leave":
 No     84.0
Yes    16.0
Name: leave, dtype: float64

Таким образом, количество клиентов, ушедщих от оператора связи «Ниединогоразрыва.ком» составляет 16 % против 84% неушедщих. Дисбаланс классов должен быть учтен в дальнейшем при обучении модели машинного обучения для задачи классификации.

In [15]:
# код тимлида для проверки
df['leave'].value_counts(normalize=True)
Out[15]:
No     0.843675
Yes    0.156325
Name: leave, dtype: float64

Исследование данных¶

Предварительный аналих данных¶

Проведем анализ данных для объединенной таблицы:

In [16]:
pd.set_option('display.max_columns', None)
data_analysis(df)
print()
print('Количество дупликатов:', df['customer_id'].duplicated().sum())
print('Количество пропусков:', df.isna().sum().sum())
print('Количество строк с пропусками:',  df[df.isna().sum(axis= 1) > 0].shape[0])
print()    
Объединенная таблица
customer_id begin_date end_date type paperless_billing payment_method monthly_charges total_charges gender senior_citizen partner dependents internet_service online_security online_backup device_protection tech_support streaming_tv streaming_movies multiple_lines leave
0 7590-VHVEG 2020-01-01 No Month-to-month Yes Electronic check 29.85 31.04 Female 0 Yes No DSL No Yes No No No No NaN No
1 5575-GNVDE 2017-04-01 No One year No Mailed check 56.95 2071.84 Male 0 No No DSL Yes No Yes No No No No No
2 3668-QPYBK 2019-10-01 No Month-to-month Yes Mailed check 53.85 226.17 Male 0 No No DSL Yes Yes No No No No No No
3 7795-CFOCW 2016-05-01 No One year No Bank transfer (automatic) 42.30 1960.6 Male 0 No No DSL Yes No Yes Yes No No NaN No
4 9237-HQITU 2019-09-01 No Month-to-month Yes Electronic check 70.70 353.5 Female 0 No No Fiber optic No No No No No No No No
Количество строк: 7043
Количество столбцов: 21

Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   customer_id        7043 non-null   object 
 1   begin_date         7043 non-null   object 
 2   end_date           7043 non-null   object 
 3   type               7043 non-null   object 
 4   paperless_billing  7043 non-null   object 
 5   payment_method     7043 non-null   object 
 6   monthly_charges    7043 non-null   float64
 7   total_charges      7043 non-null   object 
 8   gender             7043 non-null   object 
 9   senior_citizen     7043 non-null   int64  
 10  partner            7043 non-null   object 
 11  dependents         7043 non-null   object 
 12  internet_service   5517 non-null   object 
 13  online_security    5517 non-null   object 
 14  online_backup      5517 non-null   object 
 15  device_protection  5517 non-null   object 
 16  tech_support       5517 non-null   object 
 17  streaming_tv       5517 non-null   object 
 18  streaming_movies   5517 non-null   object 
 19  multiple_lines     6361 non-null   object 
 20  leave              7043 non-null   object 
dtypes: float64(1), int64(1), object(19)
memory usage: 1.2+ MB



Количество дупликатов: 0
Количество пропусков: 11364
Количество строк с пропусками: 2208

Объединенный датафрейм содержит 21 столбцов, включая созданный целевой признак. Дупликатов по 'customer_id' в датафрейме не обнаружено. Количество пропусков - 11364 шт, при этом количество строк, содержащиие пропуски, равно 2208.

Пропуски содержатся в колонках : 'internet_service', 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies', 'multiple_lines', что объясняется неиспользованием клиентами определенных услуг. Пропуски образовались в процессе объединения таблиц.

Столбец 'total_charges' представлена типом данных object, хотя он содержит количественные переменные. Необходимо перевести в тип float64.

Столбец 'senior_citizen' представлена типом данных int64, хотя он содержит категориальные переменные. Необходимо перевести в тип object, при чем перевести значения из [0, 1] в ['No', 'Yes']

Столбцы 'begin_date', 'end_date' представленs типом данных object, хотя они содержат даты. Необходимо перевести в тип datetime64.

Изменение типа данных¶

senior_citizen¶

Изменим тип данных столбца 'senior_citizen':

In [17]:
print('Количество пенсионеров до преобразования:', df[df['senior_citizen'] == 1]['senior_citizen'].count(), ', Тип данных:', df['senior_citizen'].dtype)

df['senior_citizen'] = (
    df['senior_citizen'].mask(
        df['senior_citizen'] == 0, 'No').where(
        df['senior_citizen'] == 0, 'Yes')
)
print('Количество пенсионеров после преобразования:', df[df['senior_citizen'] == 'Yes']['senior_citizen'].count(), ', Тип данных:', df['senior_citizen'].dtype)
Количество пенсионеров до преобразования: 1142 , Тип данных: int64
Количество пенсионеров после преобразования: 1142 , Тип данных: object

total_charges¶

Изменим тип данных столбца 'total_charges':

In [18]:
print('Тип данных столбца "total_charges" до преобразования:', df['total_charges'].dtype)
df['total_charges'] = pd.to_numeric(df['total_charges'],errors='coerce')
print('Тип данных столбца "total_charges" после преобразования:', df['total_charges'].dtype)
Тип данных столбца "total_charges" до преобразования: object
Тип данных столбца "total_charges" после преобразования: float64

begin_date, end_date¶

Изменим тип данных столбов 'begin_date', 'end_date':

In [19]:
print('Тип данных столбцов до преобразования:')
print('begin_date -', df['begin_date'].dtype)
print('end_date -', df['end_date'].dtype)

df['begin_date'] = pd.to_datetime(df['begin_date'], errors='ignore')
df['end_date'] = pd.to_datetime(df['end_date'], errors='coerce')

print('Тип данных столбцов после преобразования:')
print('begin_date -', df['begin_date'].dtype)
print('end_date -', df['end_date'].dtype)
Тип данных столбцов до преобразования:
begin_date - object
end_date - object
Тип данных столбцов после преобразования:
begin_date - datetime64[ns]
end_date - datetime64[ns]

Заполнение пропусков¶

total_charges¶

Изучим пропуски в столбце 'total_charges':

In [20]:
df[df['total_charges'].isna()== True].sample(5)
Out[20]:
customer_id begin_date end_date type paperless_billing payment_method monthly_charges total_charges gender senior_citizen partner dependents internet_service online_security online_backup device_protection tech_support streaming_tv streaming_movies multiple_lines leave
936 5709-LVOEQ 2020-02-01 NaT Two year No Mailed check 80.85 NaN Female No Yes Yes DSL Yes Yes Yes No Yes Yes No No
1340 1371-DWPAZ 2020-02-01 NaT Two year No Credit card (automatic) 56.05 NaN Female No Yes Yes DSL Yes Yes Yes Yes Yes No NaN No
753 3115-CZMZD 2020-02-01 NaT Two year No Mailed check 20.25 NaN Male No No Yes NaN NaN NaN NaN NaN NaN NaN No No
4380 2520-SGTTA 2020-02-01 NaT Two year No Mailed check 20.00 NaN Female No Yes Yes NaN NaN NaN NaN NaN NaN NaN No No
1082 4367-NUYAO 2020-02-01 NaT Two year No Mailed check 25.75 NaN Male No Yes Yes NaN NaN NaN NaN NaN NaN NaN Yes No

Пропуски в столбце 'total_charges' связаны с тем, что день начала контракта связан с днем выгрузки датасета ( 1 февраля 2020 года), а значит клиенты еще не оплачивали услуги телефонии или интернета. ЗХаменим пропущенные значения на '0':

In [21]:
print('Количество пропусков до заполнения:', df.total_charges.isna().sum())

df['total_charges'] = df['total_charges'].fillna(0)

print('Количество пропусков после заполнения:', df.total_charges.isna().sum())
Количество пропусков до заполнения: 11
Количество пропусков после заполнения: 0

end_date¶

Столбец 'end_date' содержит пропусков после преобразования значений в тип данных 'datetime64'. Заполним пропущенные значения временем выгрузки датасетов, равному 1 февраля 2020 года. Данное заполнение будет использоваться в дальнейшем при создании синтетических признаков.

In [22]:
print('Количество пропусков до заполнения:', df.end_date.isna().sum())

df['end_date'] = df['end_date'].fillna('2020-02-01')

print('Количество пропусков после заполнения:', df.end_date.isna().sum())
Количество пропусков до заполнения: 5942
Количество пропусков после заполнения: 0

multiple_lines¶

Столбец 'multiple_lines' содержит информацию о возможности подключения телефонного аппарата к нескольким линиям одновременно. Пропуски в столбце образовались в процессе объединения таблиц и сигнализируют о том, что клиент не пользуется услугами телефонной связи. Поэтому заменим все пропски на 'No service'.

In [23]:
print('Заполненных строк:', df.multiple_lines.isna().sum())
print()

print ('\033[1m' + 'До заполнения:' +'\033[0m')
print('Уникальные значения:', df['multiple_lines'].unique())
print('Количество строк с пропусками:', df[df.isna().sum(axis= 1) > 0].shape[0])
    
df['multiple_lines'] = df['multiple_lines'].fillna('No service')

print()
print ('\033[1m' + 'После заполнения:' +'\033[0m')
print('Уникальные значения:', df['multiple_lines'].unique())
print('Количество строк с пропусками:', df[df.isna().sum(axis= 1) > 0].shape[0])
Заполненных строк: 682

До заполнения:
Уникальные значения: [nan 'No' 'Yes']
Количество строк с пропусками: 2208

После заполнения:
Уникальные значения: ['No service' 'No' 'Yes']
Количество строк с пропусками: 1526

internet_service, online_security, online_backup, device_protection, tech_support, streaming_tv, streaming_movies¶

Рассматривая столбцы 'internet_service', 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies' видно, что они имеют одинаковое количество пропусков. Пропуски в столбце образовались в процессе объединения таблиц и сигнализируют о том, что клиент не пользуется интернет-услугами от оператора. Заменим пропуски в столбце 'internet_service' заглушкой 'No service', а в столбцах 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies' - 'No', при условии что в столбце 'internet_service' присутствуют пропуски:

In [24]:
print('Заполненных строк:', df.internet_service.isna().sum())
print()

print ('\033[1m' + 'До заполнения:' +'\033[0m')
print('Количество строк с пропусками:', df[df.isna().sum(axis= 1) > 0].shape[0])
print('Уникальные значения:')
for i in ['internet_service', 'online_security', 'online_backup', 
          'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies']:
    print(i,'-', df[i].unique())

# Заполнение 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies'
# при условии, что в 'internet_service' есть пропуски :
df.loc[df['internet_service'].isna() == True, 
       ['online_security','online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies']
      ] = df.loc[df['internet_service'].isna() == True, 
                 ['online_security','online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies']
                ].fillna('No')

# Заполнение 'internet_service':
df['internet_service'] = df['internet_service'].fillna('No service')

print()
print ('\033[1m' + 'После заполнения:' +'\033[0m')
print('Количество строк с пропусками:', df[df.isna().sum(axis= 1) > 0].shape[0])
print('Уникальные значения:')
for i in ['internet_service', 'online_security', 'online_backup', 
          'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies']:
    print(i,'-', df[i].unique())
Заполненных строк: 1526

До заполнения:
Количество строк с пропусками: 1526
Уникальные значения:
internet_service - ['DSL' 'Fiber optic' nan]
online_security - ['No' 'Yes' nan]
online_backup - ['Yes' 'No' nan]
device_protection - ['No' 'Yes' nan]
tech_support - ['No' 'Yes' nan]
streaming_tv - ['No' 'Yes' nan]
streaming_movies - ['No' 'Yes' nan]

После заполнения:
Количество строк с пропусками: 0
Уникальные значения:
internet_service - ['DSL' 'Fiber optic' 'No service']
online_security - ['No' 'Yes']
online_backup - ['Yes' 'No']
device_protection - ['No' 'Yes']
tech_support - ['No' 'Yes']
streaming_tv - ['No' 'Yes']
streaming_movies - ['No' 'Yes']

Проверим обработанный датасет:

In [25]:
data_analysis(df)
Объединенная таблица
customer_id begin_date end_date type paperless_billing payment_method monthly_charges total_charges gender senior_citizen partner dependents internet_service online_security online_backup device_protection tech_support streaming_tv streaming_movies multiple_lines leave
0 7590-VHVEG 2020-01-01 2020-02-01 Month-to-month Yes Electronic check 29.85 31.04 Female No Yes No DSL No Yes No No No No No service No
1 5575-GNVDE 2017-04-01 2020-02-01 One year No Mailed check 56.95 2071.84 Male No No No DSL Yes No Yes No No No No No
2 3668-QPYBK 2019-10-01 2020-02-01 Month-to-month Yes Mailed check 53.85 226.17 Male No No No DSL Yes Yes No No No No No No
3 7795-CFOCW 2016-05-01 2020-02-01 One year No Bank transfer (automatic) 42.30 1960.60 Male No No No DSL Yes No Yes Yes No No No service No
4 9237-HQITU 2019-09-01 2020-02-01 Month-to-month Yes Electronic check 70.70 353.50 Female No No No Fiber optic No No No No No No No No
Количество строк: 7043
Количество столбцов: 21

Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column             Non-Null Count  Dtype         
---  ------             --------------  -----         
 0   customer_id        7043 non-null   object        
 1   begin_date         7043 non-null   datetime64[ns]
 2   end_date           7043 non-null   datetime64[ns]
 3   type               7043 non-null   object        
 4   paperless_billing  7043 non-null   object        
 5   payment_method     7043 non-null   object        
 6   monthly_charges    7043 non-null   float64       
 7   total_charges      7043 non-null   float64       
 8   gender             7043 non-null   object        
 9   senior_citizen     7043 non-null   object        
 10  partner            7043 non-null   object        
 11  dependents         7043 non-null   object        
 12  internet_service   7043 non-null   object        
 13  online_security    7043 non-null   object        
 14  online_backup      7043 non-null   object        
 15  device_protection  7043 non-null   object        
 16  tech_support       7043 non-null   object        
 17  streaming_tv       7043 non-null   object        
 18  streaming_movies   7043 non-null   object        
 19  multiple_lines     7043 non-null   object        
 20  leave              7043 non-null   object        
dtypes: datetime64[ns](2), float64(2), object(17)
memory usage: 1.2+ MB


Таким образом, пропуски в датасете были обработаны путем замены на соответсвующие значения. В столбце 'multiple_lines' было заменно 682 пропуска путем замены на строковое значение 'No', в столбцах 'internet_service', 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies' было заменно по 1526 пропусков, при этом в столбце 'internet_service' путем замены на строковое значение 'No internet', в остальных - на значение 'No'.

Анализ признаков с количественными значениями¶

Проведем описательную статистику для столбцов с количественными значениями 'monthly_charges','total_charges'. Для этого построим функцию анализа признаков с количественными значениями, построения графиков плотности распределения и диаграмм размаха.

In [26]:
def analysis_quantitatives(features_list):
    for i in features_list:
        print()
        print('\033[1m' + i,':' +'\033[0m')
        print()
        print('Клиенты ушли от оператора:')
        print(df[i][df['leave'] == 'Yes'].describe())
        print()
        print('Клиенты не ушли от оператора:')
        print(df[i][df['leave'] == 'No'].describe())
        sns.set()
        lines, axes = plt.subplots(1, 2, figsize=(16, 4))
        sns.histplot(data=df, hue='leave', x=i, ax=axes[0], kde=True).set_title(i + ". Плотность распределения", fontsize=18)
        sns.boxplot(data=df, x=i, y='leave', ax=axes[1]).set_title(i + ". Диаграмма размаха", fontsize=18)
        plt.show()
In [27]:
analysis_quantitatives(['monthly_charges','total_charges'])
monthly_charges :

Клиенты ушли от оператора:
count    1101.000000
mean       75.546004
std        29.116213
min        18.400000
25%        56.150000
50%        84.200000
75%        99.500000
max       118.750000
Name: monthly_charges, dtype: float64

Клиенты не ушли от оператора:
count    5942.000000
mean       62.763455
std        29.844462
min        18.250000
25%        30.062500
50%        69.200000
75%        87.237500
max       118.600000
Name: monthly_charges, dtype: float64
total_charges :

Клиенты ушли от оператора:
count    1101.000000
mean     2371.377275
std      1581.862275
min        77.840000
25%      1048.050000
50%      2139.030000
75%      3404.910000
max      7649.760000
Name: total_charges, dtype: float64

Клиенты не ушли от оператора:
count    5942.000000
mean     2067.866420
std      2193.898483
min         0.000000
25%       374.352500
50%      1192.800000
75%      3173.837500
max      9221.380000
Name: total_charges, dtype: float64

Рассматривая распределения признаков в разрезе целевой переменной, можно отметить:

  1. Наиболее часто встречаются клиенты с ежемесячными тратами на услуги до значения 20, при этом из графика можно предположить, что клиенты с такими тратами в процентоном отношении реше уходят от оператора. В абсолютном значении наименьший отток клиентов наблюдается в районе значения 40, после которого наблюдается повышательная тендеция с пиком в 105.
  2. Наиболее часто встречаются клиенты с общими тратам до значения 300, при этом из графика можно предположить, что клиенты с такими тратами в процентоном отношении реше уходят от оператора.
  3. Диаграмма размаха и метод описательной статистики указывает на остутсвие выбросов в данных столбца с ежемесячными трататами на услуги, в то время как столбец "total_charges" имеет аномальные значения от 7500. В среднем клиенты, которые ушли от орератора, в среднем имеют выше как ежемесячные, так и общие траты.

Выведем случайным образом строки датасета с значением 'total_charges' выше 7500:

In [28]:
df[df['total_charges']>7500].sample(10)
Out[28]:
customer_id begin_date end_date type paperless_billing payment_method monthly_charges total_charges gender senior_citizen partner dependents internet_service online_security online_backup device_protection tech_support streaming_tv streaming_movies multiple_lines leave
2689 8628-MFKAX 2014-02-01 2020-02-01 Two year Yes Credit card (automatic) 116.75 8910.36 Female Yes Yes No Fiber optic Yes Yes Yes Yes Yes Yes Yes No
2515 8869-LIHMK 2014-10-01 2020-02-01 Two year Yes Bank transfer (automatic) 115.10 8029.38 Female No No No Fiber optic Yes Yes Yes Yes Yes Yes Yes No
5995 2193-SFWQW 2014-02-01 2020-02-01 Two year No Bank transfer (automatic) 111.95 8060.40 Male No Yes Yes Fiber optic Yes Yes Yes Yes Yes Yes No No
3348 2172-EJXVF 2014-03-01 2020-02-01 One year Yes Electronic check 105.90 7744.47 Female Yes No No Fiber optic No Yes Yes No Yes Yes Yes No
1280 2388-LAESQ 2014-02-01 2020-02-01 Two year Yes Bank transfer (automatic) 114.85 8765.35 Female Yes Yes No Fiber optic Yes Yes Yes Yes Yes Yes Yes No
6035 9835-ZIITK 2014-06-01 2020-01-01 One year Yes Electronic check 110.85 7649.76 Male Yes Yes No Fiber optic No Yes Yes Yes Yes Yes Yes Yes
57 5067-XJQFU 2014-08-01 2020-02-01 One year Yes Electronic check 108.45 7730.32 Male Yes Yes Yes Fiber optic No Yes Yes Yes Yes Yes Yes No
3964 2632-IVXVF 2014-06-01 2020-02-01 Two year No Credit card (automatic) 111.75 7902.96 Female No Yes Yes Fiber optic Yes No Yes Yes Yes Yes Yes No
6225 1452-UZOSF 2014-02-01 2020-02-01 Two year Yes Credit card (automatic) 106.10 7639.20 Male No Yes Yes Fiber optic Yes Yes Yes Yes Yes No Yes No
4031 8309-PPCED 2014-02-01 2020-02-01 Two year Yes Bank transfer (automatic) 110.45 8429.54 Female No Yes No Fiber optic No Yes Yes Yes Yes Yes Yes No

Из приведенных строк видно, что данные не являются выбросами, так как они объясняются большим количестовом дополнительных услуг, которыми пользуются клиенты.

Анализ признаков с категориальными значениями¶

Рассмотрим распределение признаков в датасете:

In [29]:
col_list = ['type', 'paperless_billing', 'gender', 'senior_citizen',
            'partner', 'dependents', 'payment_method', 'internet_service', 'online_security', 'online_backup',
            'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies', 'multiple_lines']
name_list = ['Тип договора', 'Выставления счёта по электронной почте', 'Пол клиента', 'Пенсионный статус',
            'Наличие супруга/супруги', 'Наличие иждивенцев', 'Способ оплаты', 'Наличие услуг Интернет', 'Межсетевой экран', 
             'Облачное хранилище файлов', 'Антивирус', 'Выделенная линия технической поддержки', 
             'Онлайн-ТВ', 'Онлайн-кинотеатр', 'Возможность подключения телефонного \n аппарата к нескольким линиям']


pic_box = plt.figure(figsize=(16,20))
 
for i in range(15):
    pic_box.add_subplot(5,3,i+1)
    sns.histplot(data=df, x=df[col_list[i]], hue='leave', multiple='dodge', shrink = 0.8)
    plt.title(str(i+1)+'.'+ name_list[i], fontsize=14)
plt.tight_layout()    

Рассмотрим распределение признаков в процентном отношении:

In [30]:
for i in range(15):
    plt.figure(figsize=(12,4))
    for b in df[col_list[i]].unique(): 
        plt.subplot(1,len(df[col_list[i]].unique()),list(df[col_list[i]].unique()).index(b)+1)
        sns.histplot(data=df[df[col_list[i]] == b], x='leave', hue ='leave',
                     shrink = 0.8, multiple='dodge', stat = 'percent', bins = 4)
        plt.title(str(i+1)+'.'+ name_list[i] + '\n- ' + b, fontsize=12)
plt.tight_layout()

Рассматривая распределения признаков в разрезе целевой переменной, можно отметить:

  1. Наиболее часто встречаемым типом договора является ежемесячный (примерно в три раза превышает остальные типы), при клиенты с данным типом договора имеют меньшую тендецнию к уходу от оператора.
  2. Наиболее часто счета выставляется по электронной почте, при такие клиенты имеют меньшую тендецнию к уходу от оператора.
  3. Доля мужчин и женщин среди клиентов одинаковая, влияние на целевой признак не прослеживается.
  4. Людей пенсионного возраста значительно меньше не пенсиннного (примерно в 5 раз), прослеживается немного больший отток клиентов не пенсионного возраста.
  5. Клиентов с супругой\супрогом примерно также как и одиноких клиентов, при этом наличие партнера в два раза сильнее влияет на отток клиентов.
  6. Клиенты без иждивенцев в два раза превышают клиентов с иждивенцами, при этом они немного меньше уходят от оператора.
  7. Наиболее часто встречаемым способом оплаты является электронный чек, при этом наименьший отток клиентов наблюдается у тех кто оплачивает почтовым чеком.
  8. Наиболее часто встречаеммой интернет-услугой является оптоволоконная связь, при этом отток клиентов у этой услуги больше, чем у DSL и у клиентов, которые не пользуются интренетом.
  9. Наиболее чаще клиенты не пользуются межсетевым экраном, заметна тендеция более высокого оттока клиентов с наличием этой услуги.
  10. Наиболее чаще клиенты не пользуются облачным хранилищем файлов, заметна тендеция более высокого оттока клиентов с наличием этой услуги.
  11. Наиболее чаще клиенты не пользуются антивирусом, заметна тендеция более высокого оттока клиентов с наличием этой услуги.
  12. Наиболее чаще клиенты не пользуются выделенной линией технической поддержки, заметна тендеция более высокого оттока клиентов с наличием этой услуги.
  13. Наиболее чаще клиенты не пользуются Онлайн-ТВ, заметна тендеция более высокого оттока клиентов с наличием этой услуги.
  14. Наиболее чаще клиенты не пользуются Онлайн-кинотеатром, заметна тендеция более высокого оттока клиентов с наличием этой услуги.
  15. Наиболее чаще клиенты не пользуются услугой подключения телефонного аппарата к нескольким линиям, заметна тендеция более высокого оттока клиентов с наличием этой услуги.

Удаление и создание признаков¶

Создание признаков¶

Из известных данных создадим следующие синтетические признаки:

  • Длительность обслуживания клиента 'duration',
  • Близость окончания ежемесячного договора 'contract_end'.

Из известных данных создадим синтетический признак "Длительность обслуживания клиента" - 'duration':

In [31]:
# Длительность обслуживания клиента 'duration'
df['duration'] = (df['end_date'] - df['begin_date'])/np.timedelta64(1,'D')

Создадим синтетический признак "Близость окончания договора" - 'contract_end'. Для каждого клиента будет сгенерировано значение в зависимости от типа его договора с оператором связи. Вставленное значение будет варьироваться от 0 до 1, где значение около 0 - это договор только заключен, около 1 - договор подходит к концу и требуется пролонгация.

Создадим функцию для генерирования синтетического признака 'contract_end' и примерим его к датасету:

In [32]:
def contract_end(data):
    if data['type'] == 'Month-to-month':
        return (data['end_date'] - data['begin_date'])/np.timedelta64(1,'M')\
        - np.floor((data['end_date'] - data['begin_date'])/np.timedelta64(1,'M')) 
    if data['type'] == 'One year':
        return (data['end_date'] - data['begin_date'])/np.timedelta64(1,'Y')\
        - np.floor((data['end_date'] - data['begin_date'])/np.timedelta64(1,'Y')) 
    if data['type'] == 'Two year':
        return (data['end_date'] - data['begin_date'])/np.timedelta64(2,'Y')\
        - np.floor((data['end_date'] - data['begin_date'])/np.timedelta64(2,'Y'))      
In [33]:
df['contract_end'] = df.apply(contract_end, axis = 1)

Анализ синтетических признаков¶

Проведем описательную статистику для синтетических признаков, построим график плотности распределения и диаграмму размаха.

In [34]:
analysis_quantitatives(['duration', 'contract_end'])
duration :

Клиенты ушли от оператора:
count    1101.000000
mean      924.863760
std       458.771309
min        28.000000
25%       577.000000
50%       915.000000
75%      1249.000000
max      2129.000000
Name: duration, dtype: float64

Клиенты не ушли от оператора:
count    5942.000000
mean      893.681084
std       716.958551
min         0.000000
25%       245.000000
50%       702.000000
75%      1523.000000
max      2314.000000
Name: duration, dtype: float64
contract_end :

Клиенты ушли от оператора:
count    1101.000000
mean        0.483135
std         0.364089
min         0.000041
25%         0.085621
50%         0.457935
75%         0.835767
max         0.999418
Name: contract_end, dtype: float64

Клиенты не ушли от оператора:
count    5942.000000
mean        0.310056
std         0.367864
min         0.000000
25%         0.033183
50%         0.063800
75%         0.625653
max         0.999418
Name: contract_end, dtype: float64

Рассматривая распределения значений в синтетических признаках в разрезе целевой переменной, можно отметить:

  1. Наибольший отток клиентов от оператора связи наблюдается при длительности пользования услугами в районе 1100 дней, при этом клиенты с длительностю договора выше 2000 практически не уходят от оператора, что вызвано их лояльностью. Клиенты, которые только заключили договор, тоже реже уходят от оператора.
  2. Подавляющее количество клиентов наблюдается в начале заключения договора. Отток клиентов наблюдается приблизительно одинаковый по всей длительности договора.
  3. Диаграмма размаха и метод описательной статистики указывает на остутсвие выбросов в данных столбца.

Удаление избыточных признаков¶

Признаки, содержащие временные значения и идентификационную информацию, являются избыточными при построении модели машинного обучения для задачи классификации. Поэтому удалим признаки 'customer_id', 'begin_date', 'end_date'.

In [35]:
df_before_delete = df.copy()
    
df.drop(['customer_id', 'begin_date', 'end_date'], axis=1, inplace = True)

Проверим обработанный датасет:

In [36]:
data_analysis(df)
Объединенная таблица
type paperless_billing payment_method monthly_charges total_charges gender senior_citizen partner dependents internet_service online_security online_backup device_protection tech_support streaming_tv streaming_movies multiple_lines leave duration contract_end
0 Month-to-month Yes Electronic check 29.85 31.04 Female No Yes No DSL No Yes No No No No No service No 31.0 0.018501
1 One year No Mailed check 56.95 2071.84 Male No No No DSL Yes No Yes No No No No No 1036.0 0.836472
2 Month-to-month Yes Mailed check 53.85 226.17 Male No No No DSL Yes Yes No No No No No No 123.0 0.041151
3 One year No Bank transfer (automatic) 42.30 1960.60 Male No No No DSL Yes No Yes Yes No No No service No 1371.0 0.753671
4 Month-to-month Yes Electronic check 70.70 353.50 Female No No No Fiber optic No No No No No No No No 153.0 0.026797
Количество строк: 7043
Количество столбцов: 20

Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 7043 entries, 0 to 7042
Data columns (total 20 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   type               7043 non-null   object 
 1   paperless_billing  7043 non-null   object 
 2   payment_method     7043 non-null   object 
 3   monthly_charges    7043 non-null   float64
 4   total_charges      7043 non-null   float64
 5   gender             7043 non-null   object 
 6   senior_citizen     7043 non-null   object 
 7   partner            7043 non-null   object 
 8   dependents         7043 non-null   object 
 9   internet_service   7043 non-null   object 
 10  online_security    7043 non-null   object 
 11  online_backup      7043 non-null   object 
 12  device_protection  7043 non-null   object 
 13  tech_support       7043 non-null   object 
 14  streaming_tv       7043 non-null   object 
 15  streaming_movies   7043 non-null   object 
 16  multiple_lines     7043 non-null   object 
 17  leave              7043 non-null   object 
 18  duration           7043 non-null   float64
 19  contract_end       7043 non-null   float64
dtypes: float64(4), object(16)
memory usage: 1.1+ MB


Таким образом, успешно удалено 3 стобца 'customer_id', 'begin_date', 'end_date' и сгенерированы 2 сентитических признака 'duration', 'contract_end'. Датасет содержит 19 признаков и 7043 объекта, пропущенных значений нет.

Корреляционный анализ¶

Проверка корреляции признаков¶

Проверку корреляций проведем с помощью библиотеки phik. Построим матрицу корреляции признаков:

In [37]:
phik_overview = df[['leave'] + [x for x in df.columns if x != 'leave']
                  ].phik_matrix(interval_cols=['monthly_charges','total_charges', 'duration', 'contract_end'])
In [38]:
phik_overview.sort_values(by='leave', ascending=False).style.background_gradient(cmap = 'coolwarm')
Out[38]:
  leave type paperless_billing payment_method monthly_charges total_charges gender senior_citizen partner dependents internet_service online_security online_backup device_protection tech_support streaming_tv streaming_movies multiple_lines duration contract_end
leave 1.000000 0.094015 0.083398 0.214832 0.226280 0.302890 0.008581 0.086159 0.226688 0.046871 0.056621 0.132594 0.229482 0.218380 0.103652 0.200198 0.222232 0.105101 0.374569 0.318624
duration 0.374569 0.634155 0.026799 0.350964 0.387727 0.848337 0.000000 0.063315 0.453688 0.198729 0.060845 0.395693 0.414982 0.426931 0.404795 0.339313 0.339313 0.347289 1.000000 0.730764
contract_end 0.318624 0.742251 0.184774 0.295072 0.302197 0.459746 0.000000 0.148757 0.326143 0.252733 0.262514 0.300852 0.215581 0.280061 0.336217 0.188384 0.188738 0.184148 0.730764 1.000000
total_charges 0.302890 0.470860 0.201703 0.335666 0.710905 1.000000 0.000000 0.135650 0.381958 0.084247 0.490081 0.522090 0.622445 0.640977 0.550065 0.641488 0.643210 0.467787 0.848337 0.459746
online_backup 0.229482 0.098884 0.196443 0.282475 0.629541 0.622445 0.009882 0.102065 0.219223 0.031533 0.233602 0.430425 1.000000 0.458211 0.445130 0.428007 0.417170 0.140081 0.414982 0.215581
partner 0.226688 0.179736 0.013218 0.243008 0.203545 0.381958 0.000000 0.016992 1.000000 0.652122 0.000000 0.221673 0.219223 0.238079 0.185993 0.193258 0.182011 0.086249 0.453688 0.326143
monthly_charges 0.226280 0.388444 0.467812 0.399526 1.000000 0.710905 0.008175 0.304985 0.203545 0.184366 0.919002 0.551621 0.629541 0.667481 0.576525 0.835340 0.833307 0.709983 0.387727 0.302197
streaming_movies 0.222232 0.069608 0.325551 0.378907 0.833307 0.643210 0.000000 0.186141 0.182011 0.058999 0.272782 0.289097 0.417170 0.589888 0.424078 0.742479 1.000000 0.170432 0.339313 0.188738
device_protection 0.218380 0.137610 0.160796 0.306866 0.667481 0.640977 0.000000 0.090686 0.238079 0.010416 0.232916 0.418474 0.458211 1.000000 0.499267 0.575536 0.589888 0.145710 0.426931 0.280061
payment_method 0.214832 0.277462 0.370495 1.000000 0.399526 0.335666 0.000000 0.292725 0.243008 0.224903 0.323886 0.262911 0.282475 0.306866 0.272101 0.377209 0.378907 0.174849 0.350964 0.295072
streaming_tv 0.200198 0.066961 0.343524 0.377209 0.835340 0.641488 0.000000 0.163120 0.193258 0.017331 0.272818 0.272186 0.428007 0.575536 0.422242 1.000000 0.742479 0.166899 0.339313 0.188384
online_security 0.132594 0.152145 0.000000 0.262911 0.551621 0.522090 0.018397 0.057028 0.221673 0.124945 0.241421 1.000000 0.430425 0.418474 0.528391 0.272186 0.289097 0.095572 0.395693 0.300852
multiple_lines 0.105101 0.244410 0.099953 0.174849 0.709983 0.467787 0.000000 0.087925 0.086249 0.011198 0.739808 0.095572 0.140081 0.145710 0.098571 0.166899 0.170432 1.000000 0.347289 0.184148
tech_support 0.103652 0.179999 0.055929 0.272101 0.576525 0.550065 0.000000 0.092565 0.185993 0.096912 0.239663 0.528391 0.445130 0.499267 1.000000 0.422242 0.424078 0.098571 0.404795 0.336217
type 0.094015 1.000000 0.106860 0.277462 0.388444 0.470860 0.000000 0.086231 0.179736 0.147680 0.505187 0.152145 0.098884 0.137610 0.179999 0.066961 0.069608 0.244410 0.634155 0.742251
senior_citizen 0.086159 0.086231 0.242133 0.292725 0.304985 0.135650 0.000000 1.000000 0.016992 0.324576 0.160702 0.057028 0.102065 0.090686 0.092565 0.163120 0.186141 0.087925 0.063315 0.148757
paperless_billing 0.083398 0.106860 1.000000 0.370495 0.467812 0.201703 0.000000 0.242133 0.013218 0.172593 0.231438 0.000000 0.196443 0.160796 0.055929 0.343524 0.325551 0.099953 0.026799 0.184774
internet_service 0.056621 0.505187 0.231438 0.323886 0.919002 0.490081 0.000000 0.160702 0.000000 0.108463 1.000000 0.241421 0.233602 0.232916 0.239663 0.272818 0.272782 0.739808 0.060845 0.262514
dependents 0.046871 0.147680 0.172593 0.224903 0.184366 0.084247 0.000000 0.324576 0.652122 1.000000 0.108463 0.124945 0.031533 0.010416 0.096912 0.017331 0.058999 0.011198 0.198729 0.252733
gender 0.008581 0.000000 0.000000 0.000000 0.008175 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 0.018397 0.009882 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
In [39]:
plot_correlation_matrix(phik_overview.values, x_labels=phik_overview.columns, y_labels=phik_overview.index, 
                        vmin=0, vmax=1, color_map='Blues', title=r'correlation $\phi_K$', fontsize_factor=1.5,
                        figsize=(16,20))
plt.tight_layout()

Построенная матрица корреляции показывает, что на целевой признак (отток клиентов) сильнее всего влияет: длительность обслуживания клиента (0,37), близость окончания договора (0,32) и количество потраченных денег (0,3). Меньше всего влияют на целевой признак: пол клиента (0,01), наличие иждивенцев (0,05) и пользование интернет-услугами (0,06). При создании моделей машинного обучения рекумендуется не использовать признаки с такой низкой корреляцией. Также при создании линейных моделей необходимо учитывать появление высокой мультиколлинеарности, которая присутствует между признаками 'internet_service' и 'monthly_charges'. Таким образом рекомендуется удалить признаки 'gender', 'dependents' и 'internet_service'.

Удаление признаков c низкой корреляцией и мультиколлиниарностью¶

Признаки, содержащие временные значения и идентификационную информацию, являются избыточными при построении модели машинного обучения для задачи классификации. Поэтому удалим признаки 'customer_id', 'begin_date', 'end_date'.

In [40]:
 df.drop(['gender', 'dependents', 'internet_service'], axis=1, inplace = True)

Проверим обработанный датасет:

In [41]:
df.columns
Out[41]:
Index(['type', 'paperless_billing', 'payment_method', 'monthly_charges',
       'total_charges', 'senior_citizen', 'partner', 'online_security',
       'online_backup', 'device_protection', 'tech_support', 'streaming_tv',
       'streaming_movies', 'multiple_lines', 'leave', 'duration',
       'contract_end'],
      dtype='object')
In [42]:
data_analysis(df)
Объединенная таблица
type paperless_billing payment_method monthly_charges total_charges senior_citizen partner online_security online_backup device_protection tech_support streaming_tv streaming_movies multiple_lines leave duration contract_end
0 Month-to-month Yes Electronic check 29.85 31.04 No Yes No Yes No No No No No service No 31.0 0.018501
1 One year No Mailed check 56.95 2071.84 No No Yes No Yes No No No No No 1036.0 0.836472
2 Month-to-month Yes Mailed check 53.85 226.17 No No Yes Yes No No No No No No 123.0 0.041151
3 One year No Bank transfer (automatic) 42.30 1960.60 No No Yes No Yes Yes No No No service No 1371.0 0.753671
4 Month-to-month Yes Electronic check 70.70 353.50 No No No No No No No No No No 153.0 0.026797
Количество строк: 7043
Количество столбцов: 17

Информация о таблице:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 7043 entries, 0 to 7042
Data columns (total 17 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   type               7043 non-null   object 
 1   paperless_billing  7043 non-null   object 
 2   payment_method     7043 non-null   object 
 3   monthly_charges    7043 non-null   float64
 4   total_charges      7043 non-null   float64
 5   senior_citizen     7043 non-null   object 
 6   partner            7043 non-null   object 
 7   online_security    7043 non-null   object 
 8   online_backup      7043 non-null   object 
 9   device_protection  7043 non-null   object 
 10  tech_support       7043 non-null   object 
 11  streaming_tv       7043 non-null   object 
 12  streaming_movies   7043 non-null   object 
 13  multiple_lines     7043 non-null   object 
 14  leave              7043 non-null   object 
 15  duration           7043 non-null   float64
 16  contract_end       7043 non-null   float64
dtypes: float64(4), object(13)
memory usage: 990.4+ KB


Таким образом, успешно удалено 3 стобца 'gender', 'dependents', 'internet_service'. Итоговый датасет содержит 16 признаков и 7043 объекта, пропущенных значений нет.

Выводы по 1 разделу¶

В процессе работы над первым разделом проекта:

  1. Поставлены цель и задачи работы;

  2. Были изучены и предварительно проанализирваны исходные таблицы, после чего таблицы были объедененны в одну общую. Итоговая таблица содержала 7043 строк и 20 колонок. Дупликатов по 'customer_id' в датафрейме не обнаружено. Количество пропусков - 11364 шт, при этом количество строк, содержащиие пропуски, равно 2208. Выявлено, что пропущенные значения возникли в процессе объединения таблиц;

  3. Названия столбцов приведены в соотвествии с правильным стилем PEP8. Создан целевой признак 'leave' (факт ухода клиента от оператора связи). Изменены типы даных в столбцах 'total_charges', 'senior_citizen', 'begin_date', 'end_date'. Пропуски обработаны путем замены на соответсвующие значения. В столбце 'total_charges' заменно 11 пропусrjd путем замены на значение '0', в столбце 'multiple_lines' - 682 пропуска на строковое значение 'No'. В столбцах 'internet_service', 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies' заменно по 1526 пропусков, при этом в столбце 'internet_service' путем замены на строковое значение 'No internet', в остальных - на значение 'No';

  4. Рассматривая распределения признаков в разрезе целевой переменной, можно отметить:

    • Наиболее часто встречаются клиенты с ежемесячными тратами на услуги до значения 20 и с общими тратам до значения 300, при этом из графика можно предположить, что клиенты с такими тратами в процентоном отношении реше уходят от оператора,
    • Наиболее частым способом выставления счета является электронная почта, типом договора - ежемесячный (примерно в три раза превышает остальные типы), способом оплаты - электронный чек, эти же значения среди всех остальных значений рассматриваемых признаков имеют большее влияние на отток клиентов от оператора,
    • Людей пенсионного возраста значительно меньше не пенсиннного (примерно в 5 раз), клиенты без иждивенцев в два раза превышают клиентов с иждивенцами, эти же значения оказывают сильнее влияние на целевой признак. Доля мужчин и женщин среди клиентов одинаковая, влияние на целевой признак не прослеживается,
    • Наиболее часто встречаеммой интернет-услугой является оптоволоконная связь, при этом отток клиентов у этой услуги больше, чем у DSL и у клиентов, которые не пользуются интренетом. Чаще всего клиенты не пользуются межсетевым экраном, облачным хранилищем файлов, антивирусом, выделенной линией технической поддержки,Онлайн-ТВ, Онлайн-кинотеатром, услугой подключения телефонного аппарата к нескольким линиям. Заметна тендеция более высокого оттока клиентов с наличием этих услуг,
    • Наибольший отток клиентов от оператора связи наблюдается при длительности пользования услугами в районе 1100 дней, при этом клиенты с длительностю договора выше 2000 практически не уходят от оператора, что вызвано их лояльностью. Клиенты, которые только заключили договор, тоже реже уходят от оператора;
  1. Были созданы 2 синтетических признака: длительность обслуживания клиента 'duration', близость окончания договора 'contract_end'.

  2. Построенная матрица корреляции с помощью библиотеки phik показывала, что на целевой признак (отток клиентов) сильнее всего влияет: длительность обслуживания клиента (0,37), близость окончания договора (0,32) и количество потраченных денег (0,3). Меньше всего влияют на целевой признак: пол клиента (0,01), наличие иждивенцев (0,05) и пользование интернет-услугами (0,06). Также имеется высокая мультиколлинеарность между признаками 'internet_service' и 'monthly_charges'.

  3. Из итогового датасета были удалены следующие избыточные признаки, признаки, имеющие низкую корреляцию с целевым признаком и признаки с высокой мультиколлинеарностью:

    • 'customer_id' - уникальные номер клиента,
    • 'begin_date' - дата начала пользования услугами,
    • 'end_date' - дата окончания пользования услугами,
    • 'gender' - пол,
    • 'internet_service' - наличие услуг Интернет,
    • 'Dependents' - наличие иждивенцев.

Итоговый датасет содержит 16 признаков и 7043 объекта, пропущенных значений нет.

Разбиение датасета на обучающую и тестовую выборки¶

Определим в качестве целоевого признака столбец 'leave':

In [43]:
target = df['leave']
features = df.drop(['leave'], axis=1)

Перекодируем целевой признак в числовые значения ['0','1']

In [44]:
print('Количество уникальных значений до замены:')
print(target.value_counts())

target = target.replace(['No', 'Yes'], [0, 1])

print()
print('Количество уникальных значений после замены:')
print(target.value_counts())
Количество уникальных значений до замены:
No     5942
Yes    1101
Name: leave, dtype: int64

Количество уникальных значений после замены:
0    5942
1    1101
Name: leave, dtype: int64

Разделим признаки на обучающие и тестовые выборки в соотношении 3 к 1:

In [45]:
features_train, features_test, target_train, target_test = train_test_split(
    features, target, test_size=0.33, random_state=140823, stratify=target, shuffle = True)

print('"features_train":', features_train.shape[0],'объектов,', 
      100 - round((features.shape[0] - features_train.shape[0]) * 100 / features.shape[0]), "%" )
print('"target_train":', target_train.shape[0],'объектов,', 
      100 - round((target.shape[0] - target_train.shape[0]) * 100 / target.shape[0]), "%" )
print('"features_test":', features_test.shape[0],'объектов,', 
      100 - round((features.shape[0] - features_test.shape[0]) * 100 / features.shape[0]), "%" )
print('"target_test":', target_test.shape[0],'объектов,', 
      100 - round((target.shape[0] - target_test.shape[0]) * 100 / target.shape[0]), "%" )
"features_train": 4718 объектов, 67 %
"target_train": 4718 объектов, 67 %
"features_test": 2325 объектов, 33 %
"target_test": 2325 объектов, 33 %

Кодирование и масштабирование признаков¶

Определим числовые и категориальные признаки:

In [46]:
numerical = features_train.select_dtypes(include=[np.number]).columns.tolist()
print('Числовые признаки:', numerical)
print()
categorical = features_train.select_dtypes(include='object').columns.tolist()
print('Категориальные признаки:', categorical)
Числовые признаки: ['monthly_charges', 'total_charges', 'duration', 'contract_end']

Категориальные признаки: ['type', 'paperless_billing', 'payment_method', 'senior_citizen', 'partner', 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies', 'multiple_lines']

Составим Pipiline для кодирования и масштабирования признаков, при чем OneHotEncoder будет использоваться для линейных моделей, OrdinalEncoder - для моделей на основе дерева решений:

In [47]:
numerical_transformer = StandardScaler() 
categorical_transformer_ohe = OneHotEncoder(sparse=False, handle_unknown='ignore')
categorical_transformer_ord_encoder = OrdinalEncoder()

# Делаем ColumnTransformer для категориальных переменных с OrdinalEncoder
preprocessor_ohe = ColumnTransformer(
    transformers=[
        ('num', numerical_transformer, numerical),
        ('cat', categorical_transformer_ohe, categorical)], verbose_feature_names_out=True)

# Делаем ColumnTransformer для категориальных переменных с OrdinalEncoder
preprocessor_ord_encoder = ColumnTransformer(
    transformers=[
        ('num', numerical_transformer, numerical),
        ('cat', categorical_transformer_ord_encoder, categorical)], verbose_feature_names_out=True)

Выбор и обучение моделей машинного обучения на кросс-валидации¶

Для обучения выбрем следующие модели:

  • LogisticRegression,
  • RandomForestClassifier,
  • LightGBM Classifier,
  • CatBoostClassifier.

Обучение и оценка моделей будет производится через перекрестную проверку с использованием 3 блоков для кросс-валидации.

LogisticRegression¶

In [48]:
%%time

#  Составляем pipeline:

pipeline_LR = Pipeline([
    ("preprocessor_ohe", preprocessor_ohe),
    ('model_LR', LogisticRegression(random_state=140823,
                                    penalty='elasticnet',
                                    solver='saga',
                                    class_weight='balanced'))
])

param_grid = {'model_LR__C': range(1,15,2),  
              'model_LR__l1_ratio': np.arange(0, 1, 0.2)}


tuning_model_LR = RandomizedSearchCV(pipeline_LR,
                                      param_grid, 
                                      cv=3, 
                                      n_jobs=-1, 
                                      scoring='roc_auc', 
                                      verbose=0,
                                      n_iter=50,
                                      random_state=140823,
                                     )

tuning_model_LR.fit(features_train, target_train)

print(f"Наилучшая метрика 'roc_auc', равная {round(tuning_model_LR.best_score_,2)},\
достигается при параметрах: {tuning_model_LR.best_params_}")  
Наилучшая метрика 'roc_auc', равная 0.79,достигается при параметрах: {'model_LR__l1_ratio': 0.0, 'model_LR__C': 1}
CPU times: total: 828 ms
Wall time: 9.06 s

Наилучшая метрика 'roc_auc', равная 0.79,достигается при параметрах:

  • 'l1_ratio': 0.0,
  • 'C': 1

RandomForestClassifier¶

In [49]:
%%time

#  Составляем pipeline:

pipeline_RFC = Pipeline([
    ("preprocessor_ord_encoder", preprocessor_ord_encoder),
    ('model_RFC', RandomForestClassifier(random_state=140823,
                                        class_weight='balanced'))
])

param_grid = {
    'model_RFC__n_estimators': range (10, 200, 5),
    'model_RFC__max_depth': range (1,31, 2),
    'model_RFC__min_samples_leaf': range (1,11,2),
    'model_RFC__min_samples_split': range (1,21,2)}

tuning_model_RFC = RandomizedSearchCV(pipeline_RFC,
                                      param_grid, 
                                      cv=3, 
                                      n_jobs=-1, 
                                      scoring='roc_auc', 
                                      verbose=1,
                                      n_iter=100,
                                      random_state=140823,
                                     )


tuning_model_RFC.fit(features_train, target_train)

print(f"Наилучшая метрика 'roc_auc', равная {round(tuning_model_RFC.best_score_,2)},\
достигается при параметрах: {tuning_model_RFC.best_params_}")  
Fitting 3 folds for each of 100 candidates, totalling 300 fits
Наилучшая метрика 'roc_auc', равная 0.88,достигается при параметрах: {'model_RFC__n_estimators': 60, 'model_RFC__min_samples_split': 9, 'model_RFC__min_samples_leaf': 1, 'model_RFC__max_depth': 11}
CPU times: total: 2.84 s
Wall time: 1min 3s

Наилучшая метрика 'roc_auc', равная 0.88, достигается при параметрах:

  • 'n_estimators': 60,
  • 'min_samples_split': 9,
  • 'min_samples_leaf': 1,
  • 'max_depth': 11

LGBMClassifier¶

In [50]:
%%time

pipeline_LGBM = Pipeline([
    ("preprocessor_ord_encoder", preprocessor_ord_encoder),
    ('model_LGBM', LGBMClassifier(random_state=140823,
                                 class_weight='balanced'))
])

param_grid = {'model_LGBM__learning_rate': np.arange(0.01, 0.21, 0.05),
              'model_LGBM__num_leaves': np.arange(10, 510, 10),
              'model_LGBM__max_depth': np.arange(1, 15, 1),
              'model_LGBM__n_estimators': range (10, 1000, 10)}


tuning_model_LGBM = RandomizedSearchCV(pipeline_LGBM,
                                      param_grid, 
                                      cv=3, 
                                      n_jobs=-1, 
                                      scoring='roc_auc', 
                                      verbose=1,
                                      n_iter=100,
                                      random_state=140823
                                     )


tuning_model_LGBM.fit(features_train, target_train)

print(f"Наилучшая метрика 'roc_auc', равная {round(tuning_model_LGBM.best_score_,2)},\
достигается при параметрах: {tuning_model_LGBM.best_params_}")  
Fitting 3 folds for each of 100 candidates, totalling 300 fits
Наилучшая метрика 'roc_auc', равная 0.9,достигается при параметрах: {'model_LGBM__num_leaves': 390, 'model_LGBM__n_estimators': 410, 'model_LGBM__max_depth': 2, 'model_LGBM__learning_rate': 0.16000000000000003}
CPU times: total: 2.64 s
Wall time: 1min 22s

Наилучшая метрика 'roc_auc', равная 0.9, достигается при параметрах:

  • 'num_leaves': 390,
  • 'n_estimators': 410,
  • 'max_depth': 2,
  • 'learning_rate': 0.16

CatBoostClassifier¶

In [51]:
%%time

pipeline_CBL = Pipeline([
    ("preprocessor_ord_encoder", preprocessor_ord_encoder),
    ('model_CBL', CatBoostClassifier(random_state=140823))
])

param_grid = {
    'model_CBL__depth': np.arange(1, 15, 1),
    'model_CBL__learning_rate':  np.arange(0.01, 0.21, 0.05),
    'model_CBL__iterations':  range (10, 150, 10),
    'model_CBL__l2_leaf_reg': np.arange(1, 15, 1)
}


tuning_model_CBL = RandomizedSearchCV(pipeline_CBL,
                                      param_grid, 
                                      cv=3, 
                                      n_jobs=-1, 
                                      scoring='roc_auc', 
                                      verbose=0,
                                      n_iter=30,
                                      random_state=140823
                                     )


tuning_model_CBL.fit(features_train, target_train)

print(f"Наилучшая метрика 'roc_auc', равная {round(tuning_model_CBL.best_score_,2)},\
достигается при параметрах: {tuning_model_CBL.best_params_}")  
0:	learn: 0.6104719	total: 152ms	remaining: 18.1s
1:	learn: 0.5579133	total: 155ms	remaining: 9.15s
2:	learn: 0.5103059	total: 176ms	remaining: 6.88s
3:	learn: 0.4713050	total: 186ms	remaining: 5.4s
4:	learn: 0.4411582	total: 196ms	remaining: 4.5s
5:	learn: 0.4134217	total: 209ms	remaining: 3.96s
6:	learn: 0.3911923	total: 226ms	remaining: 3.65s
7:	learn: 0.3737332	total: 236ms	remaining: 3.3s
8:	learn: 0.3583523	total: 243ms	remaining: 3s
9:	learn: 0.3462995	total: 252ms	remaining: 2.77s
10:	learn: 0.3354658	total: 262ms	remaining: 2.6s
11:	learn: 0.3264000	total: 270ms	remaining: 2.43s
12:	learn: 0.3178844	total: 278ms	remaining: 2.29s
13:	learn: 0.3112632	total: 285ms	remaining: 2.16s
14:	learn: 0.3054442	total: 290ms	remaining: 2.03s
15:	learn: 0.2999026	total: 296ms	remaining: 1.93s
16:	learn: 0.2951865	total: 302ms	remaining: 1.83s
17:	learn: 0.2914498	total: 307ms	remaining: 1.74s
18:	learn: 0.2877603	total: 312ms	remaining: 1.66s
19:	learn: 0.2847123	total: 318ms	remaining: 1.59s
20:	learn: 0.2820844	total: 325ms	remaining: 1.53s
21:	learn: 0.2784046	total: 329ms	remaining: 1.47s
22:	learn: 0.2760142	total: 334ms	remaining: 1.41s
23:	learn: 0.2735383	total: 339ms	remaining: 1.36s
24:	learn: 0.2718490	total: 345ms	remaining: 1.31s
25:	learn: 0.2699109	total: 349ms	remaining: 1.26s
26:	learn: 0.2676971	total: 361ms	remaining: 1.24s
27:	learn: 0.2657717	total: 367ms	remaining: 1.21s
28:	learn: 0.2647290	total: 375ms	remaining: 1.18s
29:	learn: 0.2636886	total: 383ms	remaining: 1.15s
30:	learn: 0.2619089	total: 393ms	remaining: 1.13s
31:	learn: 0.2605894	total: 397ms	remaining: 1.09s
32:	learn: 0.2590232	total: 401ms	remaining: 1.06s
33:	learn: 0.2572242	total: 407ms	remaining: 1.03s
34:	learn: 0.2559326	total: 411ms	remaining: 998ms
35:	learn: 0.2545307	total: 415ms	remaining: 968ms
36:	learn: 0.2533173	total: 419ms	remaining: 940ms
37:	learn: 0.2519097	total: 423ms	remaining: 914ms
38:	learn: 0.2510236	total: 427ms	remaining: 888ms
39:	learn: 0.2500120	total: 432ms	remaining: 863ms
40:	learn: 0.2487785	total: 436ms	remaining: 840ms
41:	learn: 0.2471345	total: 440ms	remaining: 818ms
42:	learn: 0.2459589	total: 444ms	remaining: 796ms
43:	learn: 0.2443147	total: 449ms	remaining: 775ms
44:	learn: 0.2435919	total: 453ms	remaining: 755ms
45:	learn: 0.2425826	total: 459ms	remaining: 738ms
46:	learn: 0.2418189	total: 463ms	remaining: 719ms
47:	learn: 0.2409690	total: 467ms	remaining: 701ms
48:	learn: 0.2392035	total: 472ms	remaining: 684ms
49:	learn: 0.2385530	total: 477ms	remaining: 667ms
50:	learn: 0.2371725	total: 481ms	remaining: 651ms
51:	learn: 0.2361233	total: 486ms	remaining: 635ms
52:	learn: 0.2350281	total: 491ms	remaining: 620ms
53:	learn: 0.2342703	total: 495ms	remaining: 605ms
54:	learn: 0.2334841	total: 500ms	remaining: 591ms
55:	learn: 0.2320627	total: 505ms	remaining: 577ms
56:	learn: 0.2304878	total: 509ms	remaining: 563ms
57:	learn: 0.2296375	total: 514ms	remaining: 549ms
58:	learn: 0.2285712	total: 518ms	remaining: 536ms
59:	learn: 0.2274190	total: 523ms	remaining: 523ms
60:	learn: 0.2267351	total: 527ms	remaining: 510ms
61:	learn: 0.2257824	total: 532ms	remaining: 497ms
62:	learn: 0.2252382	total: 536ms	remaining: 485ms
63:	learn: 0.2241388	total: 540ms	remaining: 473ms
64:	learn: 0.2232916	total: 545ms	remaining: 461ms
65:	learn: 0.2225542	total: 549ms	remaining: 449ms
66:	learn: 0.2221412	total: 554ms	remaining: 438ms
67:	learn: 0.2211160	total: 562ms	remaining: 429ms
68:	learn: 0.2199636	total: 567ms	remaining: 419ms
69:	learn: 0.2189675	total: 572ms	remaining: 408ms
70:	learn: 0.2185556	total: 576ms	remaining: 397ms
71:	learn: 0.2182530	total: 581ms	remaining: 387ms
72:	learn: 0.2174748	total: 589ms	remaining: 379ms
73:	learn: 0.2170054	total: 595ms	remaining: 370ms
74:	learn: 0.2162571	total: 601ms	remaining: 360ms
75:	learn: 0.2154745	total: 605ms	remaining: 350ms
76:	learn: 0.2146147	total: 611ms	remaining: 341ms
77:	learn: 0.2141877	total: 616ms	remaining: 332ms
78:	learn: 0.2135819	total: 620ms	remaining: 322ms
79:	learn: 0.2125027	total: 625ms	remaining: 312ms
80:	learn: 0.2116462	total: 629ms	remaining: 303ms
81:	learn: 0.2109381	total: 633ms	remaining: 294ms
82:	learn: 0.2103705	total: 638ms	remaining: 284ms
83:	learn: 0.2098662	total: 643ms	remaining: 276ms
84:	learn: 0.2089565	total: 647ms	remaining: 266ms
85:	learn: 0.2080174	total: 652ms	remaining: 258ms
86:	learn: 0.2074941	total: 656ms	remaining: 249ms
87:	learn: 0.2071838	total: 661ms	remaining: 240ms
88:	learn: 0.2061283	total: 665ms	remaining: 232ms
89:	learn: 0.2050620	total: 669ms	remaining: 223ms
90:	learn: 0.2043908	total: 673ms	remaining: 214ms
91:	learn: 0.2036915	total: 677ms	remaining: 206ms
92:	learn: 0.2027861	total: 682ms	remaining: 198ms
93:	learn: 0.2023744	total: 687ms	remaining: 190ms
94:	learn: 0.2018991	total: 691ms	remaining: 182ms
95:	learn: 0.2014200	total: 695ms	remaining: 174ms
96:	learn: 0.2007151	total: 699ms	remaining: 166ms
97:	learn: 0.2001278	total: 704ms	remaining: 158ms
98:	learn: 0.1995144	total: 709ms	remaining: 150ms
99:	learn: 0.1990695	total: 713ms	remaining: 143ms
100:	learn: 0.1982690	total: 719ms	remaining: 135ms
101:	learn: 0.1980452	total: 723ms	remaining: 128ms
102:	learn: 0.1973501	total: 727ms	remaining: 120ms
103:	learn: 0.1970446	total: 731ms	remaining: 113ms
104:	learn: 0.1967605	total: 736ms	remaining: 105ms
105:	learn: 0.1963457	total: 740ms	remaining: 97.8ms
106:	learn: 0.1958531	total: 745ms	remaining: 90.5ms
107:	learn: 0.1954689	total: 749ms	remaining: 83.2ms
108:	learn: 0.1945782	total: 753ms	remaining: 76ms
109:	learn: 0.1940821	total: 758ms	remaining: 68.9ms
110:	learn: 0.1934222	total: 762ms	remaining: 61.8ms
111:	learn: 0.1927274	total: 768ms	remaining: 54.8ms
112:	learn: 0.1920778	total: 772ms	remaining: 47.8ms
113:	learn: 0.1916859	total: 777ms	remaining: 40.9ms
114:	learn: 0.1911354	total: 781ms	remaining: 34ms
115:	learn: 0.1906868	total: 788ms	remaining: 27.2ms
116:	learn: 0.1902897	total: 796ms	remaining: 20.4ms
117:	learn: 0.1896907	total: 802ms	remaining: 13.6ms
118:	learn: 0.1890179	total: 807ms	remaining: 6.78ms
119:	learn: 0.1886750	total: 811ms	remaining: 0us
Наилучшая метрика 'roc_auc', равная 0.89,достигается при параметрах: {'model_CBL__learning_rate': 0.16000000000000003, 'model_CBL__l2_leaf_reg': 2, 'model_CBL__iterations': 120, 'model_CBL__depth': 7}
CPU times: total: 2.58 s
Wall time: 5min 53s

Наилучшая метрика 'roc_auc', равная 0.89, достигается при параметрах:

  • 'learning_rate': 0.16,
  • 'iterations': 120,
  • 'l2_leaf_reg': 2,
  • 'depth': 7

Выбор лучшей модели по метрике ROC-AUC на кросс-валидации¶

In [52]:
total = [["LogisticRegression:  ", round(tuning_model_LR.best_score_,3)],
             ["RandomForestClassifier: ", round(tuning_model_RFC.best_score_,3)],
             ["LightGBMClassifier:  ", round(tuning_model_LGBM.best_score_,3)],
             ["CatBoostClassifier:  ", round(tuning_model_CBL.best_score_,3)]
            ]
total= pd.DataFrame(total, columns=["модель","ROC-AUC"])
total = total.set_index('модель')
total.index.names = [None]

total
Out[52]:
ROC-AUC
LogisticRegression: 0.790
RandomForestClassifier: 0.875
LightGBMClassifier: 0.897
CatBoostClassifier: 0.894

Mодели LightGBMClassifier и CatBoostClassifier оказались наиболее точными по метрике ROC-AUC со значениями 0.897 и 0.894 соответсвенно. Худший результат показала модель линейной регрессии - 0,79. RandomForestClassifier показала значение ROC-AUC, равной 0.875. Лучшей моделью выбираем LightGBMClassifier

In [53]:
best_model = tuning_model_LGBM

Оценка качества лучшей модели на тестовой выборке¶

Проверим лучшую модель (LightGBMClassifier) на тестовой выборке:

In [54]:
probabilities_test = best_model.predict_proba(features_test)
probabilities_one_test = probabilities_test[:, 1]

auc_roc = roc_auc_score(target_test,probabilities_one_test)

print('Метрика ROC-AUC наилучшей модели равна', round(auc_roc,2))
Метрика ROC-AUC наилучшей модели равна 0.92

Метрика ROC-AUC модели LGBMClassifier на тестовой выборке составила 0.92, что выше условия задания - метрика ROC-AUC на тестовой выборке должна показать 0.85, поэтому цель задания выполнена. Рассмотрим более подробно метрики лучшей модели.

Построение и анализ ROC-кривой, accuracy, матрицы ошибок и важности признаков лучшей модели на тестовой выборке.¶

ROC-кривая¶

Построим ROC-кривую для лучшей модели и изобразите её на графике:

In [55]:
# Определим долю ложноположительных ответов (FPR) и истинно положительных ответов (TPR) лучшей модели:

fpr, tpr, thresholds = roc_curve(target_test,probabilities_one_test) 

# ROC-кривая лучшей модели:
plt.figure(figsize=(8, 5))
sns.lineplot(x=fpr, y=tpr)

# ROC-кривая случайной модели: 
sns.lineplot(x=[0, 1], 
             y=[0, 1], 
             linestyle='--')\
.set(xlabel='Ложноположительный результат', 
     ylabel='Истинноположительный результат',  
     title = 'ROC-кривая')

plt.xlim(0,1) 
plt.ylim(0,1)
plt.legend(['Лучшая модель', 'Случайная модель'])
plt.show()

На графике по горизонтали показана доля ложноположительных ответов (False Positive Rate), по вертикали - доля истинно положительных ответов (True Positive Rate). Для модели, которая всегда отвечает случайно, ROC-кривая представлена прямой оранжевой пунктирной линией. Касательно качества модели на графике, если модель не делает ошибок, то кривая будет стремиться к точке (0.0,1.0), в противном случае, AUC-ROC стремиться к 0.5, то есть случайно выдавать вероятность классов. Полученная площадь под кривой метрика AUC-ROC в 0.92 говорит о том, что рассматриваемая модель хорошо предсказывает значения.

Accuracy¶

Рассчитаем долю правильных ответов лучшей модели для тестовой выборки - Accuracy:

In [56]:
predictions = best_model.predict(features_test)
print('Метрика Accuracy наилучшей модели равна', round(accuracy_score(target_test, predictions),4))
Метрика Accuracy наилучшей модели равна 0.8404

Accuracy показывает отношение количества правильных прогнозов к их общему количеству. Accuracy лучшей модели, равная 0.8404, означает, что модель с точностью 84,04% делает верный прогноз.

Матрица ошибок¶

Наглядно представить результаты вычислений метрик точности и полноты позволяет матрица ошибок. Матрица ошибок формируется следующим образом: по диагонали от верхнего левого угла выстроены правильные прогнозы, вне главной диагонали — ошибочные варианты и содержит:

  • Истинно отрицательные ответы (True Negative) в левом верхнем углу,
  • Истинно положительные ответы (True Positive) в правом нижнем углу,
  • Ложноположительные ответы (False Positive) в правом верхнем углу,
  • Ложноотрицательные ответы (False Negative) в левом нижнем углу.
In [57]:
cm = confusion_matrix(target_test,predictions)
cm
Out[57]:
array([[1665,  297],
       [  74,  289]], dtype=int64)

Визуализируем матрицу ошибок:

In [58]:
plt.figure(figsize=(6, 6))

sns.heatmap(cm, annot=True, linewidth=.5, fmt=".0f", cbar=False)
plt.title("Матрица ошибок", fontsize=20)
plt.xlabel('Предсказания', fontsize=14)
plt.ylabel('Истинный класс', fontsize=14)
plt.show()

Цель задачи состоит в максимизации истинно положительных и истинно отрицательных ответов и минимизации ложноположительных и ложноотрицательных ответов. Лучшая модель LightGBMClassifier выдает 1665 истинно положительных и 289 истинно отрицательных ответов, при этом на долю ложных приходится 297 положительных и 74 отрицательных ответов соответственно.

Важность признаков¶

Выведем таблицу с важностью признаков:

In [59]:
feature_importance = [best_model.best_estimator_[0].transformers_[0][2] + best_model.best_estimator_[0].transformers[1][2],
          list(best_model.best_estimator_[1].feature_importances_)]
         

feature_importance = pd.DataFrame(feature_importance).transpose()

feature_importance.columns =['Признаки', 'Важность']
feature_importance = feature_importance.set_index('Признаки', drop=True).sort_values(by='Важность', ascending=False)
feature_importance
Out[59]:
Важность
Признаки
contract_end 423
duration 301
total_charges 172
monthly_charges 161
payment_method 38
type 32
partner 22
online_backup 16
tech_support 13
multiple_lines 13
senior_citizen 12
device_protection 9
paperless_billing 7
streaming_movies 6
online_security 3
streaming_tv 1

Визуализируем таблицу:

In [60]:
plt.figure(figsize=(8, 4))

sns.barplot(feature_importance, y = feature_importance.index, x = 'Важность')
plt.title('Важность признаков', fontsize = 16)
plt.show()

Таким образом, наибольшее влияние на прогнозирование модели оказывают синтетические признаки: близость окончания ежемесячного договора 'contract_end' (423) и длительность обслуживания клиента 'duration' (301) . Также сильное влияение имеют признаки с количественными значениями: ежемесячные траты на услуги 'monthly_charges' (161) и потраченные деньги на услуги 'total_charges' (172). Наименьшее влияние оказывают услуги онлайн-ТВ (1) и межсетевого экрана (3).

Выводы по 2 разделу¶

Датасет признаков разделен на обучающие и тестовые выборки в соотношении 3 к 1. Составлен Pipiline для кодирования и масштабирования признаков, при чем OneHotEncoder используется для линейных моделей, OrdinalEncoder - для моделей на основе дерева решений. Для обучения были выбраны следующие модели: LogisticRegression, RandomForestClassifier, LightGBM Classifier, CatBoostClassifier.

Обучение и оценка моделей производилась через перекрестную проверку с использованием 3 блоков для кросс-валидации. Mодели LightGBMClassifier и CatBoostClassifier оказались наиболее точными по метрике ROC-AUC со значениями 0.897 и 0.894 соответсвенно. Худший результат показала модель линейной регрессии - 0,79. RandomForestClassifier показала значение ROC-AUC, равной 0.88.

Лучшей моделью была выбрана LightGBMClassifier с параметрами: 'num_leaves': 390, 'n_estimators': 410, 'max_depth': 2, 'learning_rate': 0.16. Метрика ROC-AUC модели LGBMClassifier на тестовой выборке составила 0.92, что выше условия задания - метрика ROC-AUC на тестовой выборке должна показать 0.85, поэтому цель задания выполнена. Дополнительно построена ROC-кривая для модели.

Рассчитана доля правильных ответов Accuracy, равная 0.84, построена матрица ошибок. Согласно матрицы ошибок, модель LightGBMClassifier выдает 1665 истинно положительных и 289 истинно отрицательных ответов, при этом на долю ложных приходится 297 положительных и 74 отрицательных ответов соответственно.

Рассмотрена важность признаков. Наибольшее влияние на прогнозирование модели оказывают синтетические признаки: близость окончания ежемесячного договора 'contract_end' (423) и длительность обслуживания клиента 'duration' (301). Также сильное влияение имеют признаки с количественными значениями: ежемесячные траты на услуги 'monthly_charges' (161) и потраченные деньги на услуги 'total_charges' (172). Наименьшее влияние оказывают услуги онлайн-ТВ (1) и межсетевого экрана (3).

✔️ Ревью 4: Вадим, самый интересный этап проекта пройден. Остался самый приятный и не менее ответственный - написание хорошего отчета.

Отчет о проделанной работе¶

In [61]:
# Функции для построения графиков

# Анализ дисбаланса классов
def class_imbalance(data, target):
    (data[target].value_counts()/data[target].count()*100).plot(
        kind='bar', 
        grid = True
    ).set(
        ylabel = 'Относительное количество, %', 
        xlabel = 'Факт ухода клиента: \n "No" - Клиент не ушел, "Yes" - Клиент ушел', 
        title = 'Распределение оставшихся и ушедщих клиентов' 
    )
    plt.show()

# Анализ признаков с количественными значениями
def analysis_сat_report(data, cat_col_list, cat_name_list):
    for i in range(15):
        plt.figure(figsize=(12,4))
        for b in data[col_list[i]].unique(): 
            plt.subplot(1,len(data[col_list[i]].unique()),list(data[col_list[i]].unique()).index(b)+1)
            sns.histplot(data=data[data[col_list[i]] == b], x='leave', hue ='leave',
                         shrink = 0.8, multiple='dodge', stat = 'percent', bins = 4)
            plt.title(str(i+1)+'.'+ name_list[i] + '\n- ' + b, fontsize=12)
    plt.tight_layout()

#  Анализ признаков с категориальными значениями
def analysis_num_report(data, num_list):
    for i in num_list:
        print()
        print('\033[1m' + i,':' +'\033[0m')
        print()
        sns.set()
        lines, axes = plt.subplots(1, 2, figsize=(16, 4))
        sns.histplot(data=data, hue='leave', x=i, ax=axes[0], kde=True).set_title(i + ". Плотность распределения", fontsize=18)
        sns.boxplot(data=data, x=i, y='leave', ax=axes[1]).set_title(i + ". Диаграмма размаха", fontsize=18)
        plt.show()
        
#  Создание списков для функций     
cat_col_list = ['type', 'paperless_billing', 'gender', 'senior_citizen',
            'partner', 'dependents', 'payment_method', 'internet_service', 'online_security', 'online_backup',
            'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies', 'multiple_lines']
cat_name_list = ['Тип договора', 'Выставления счёта по электронной почте', 'Пол клиента', 'Пенсионный статус',
            'Наличие супруга/супруги', 'Наличие иждивенцев', 'Способ оплаты', 'Наличие услуг Интернет', 'Межсетевой экран', 
             'Облачное хранилище файлов', 'Антивирус', 'Выделенная линия технической поддержки', 
             'Онлайн-ТВ', 'Онлайн-кинотеатр', 'Возможность подключения телефонного \n аппарата к нескольким линиям']

num_list = ['monthly_charges','total_charges', 'duration', 'contract_end']

Выполнение плана¶

В процессе выполнения работы были выполнены следующие задачи:

  • Первоначальное ознакомление с данными
  • Объединение данных
  • Выделение целевого признака
  • Исследование и обработка данных
  • Удаление и создание признаков
  • Корреляционный анализ
  • Разбиение датасета на обучающую и тестовую выборки
  • Кодирование и масштабирование признаков
  • Выбор и обучение разных моделей машинного обучения на кросс-валидации
  • Выбор лучшей модели по метрике ROC-AUC на кросс-валидации
  • Оценка качества лучшей модели на тестовой выборке
  • Построение и анализ ROC-кривой, accuracy, матрицы ошибок и важности признаков лучшей модели на тестовой выборке.
  • Отчет о проделанной работе

Задачи выполнены полностью в соответсвии с планом, за исключением: Удаление признаков осуществлялось также и после корреляционного анализа; На этапе кодирования и масштабирования признаков был написан конвейер (Pipline) для этой цели, а сам процесс производился на следующем этапе при обучении моделей на кросс-валидации.

Трудности, возникшие при выполнении работы:¶

Основные трудности, возникшие при выполнении работы, связана с предобработкой исходных данных, из которых следует выделить:

  • Некоторые столбцы исходных данных содержали типы данных, препятствующие дальнейшему анализу и обучению моделей машинного обучения. Типы данных были изменены на соответсвующие информации, представленной столбцами.
  • При объединении исходных таблиц и преобразовании типов данных появились пропущенные значения. Пропуски были заполнены значениями, соответсвующими причинам возникновения.
  • Присутствует дисбаланс классов целевого признака "Уход клиента оператора связи", способный оказать негативное влияние на обучение моделей. Дисбаланс классов убран за счет взвешивания классов (class_weight=‘balanced’)
  • В изначальных данных наблюдалась мультиколлинеарность и содержались признаки, имеющий низкую корреляцию с целевым признаком. Соответсвующие признаки были удалены из датасета, применяемого для обучения моделей.

Ключевые шаги в решении задачи¶

Подготовка данных¶

  1. Первые шаги в соотвествии с планом заключались в предварительном анализе исходных таблиц в количестве 3 штук. Таблицы были объедененны в одну общую. Нзвания столбцов приведены в соответсвии с правилами правильного стиля PEP8. Предварительный анализ показал, что итоговая таблица содержала 7043 строк и 20 колонок. Дупликатов по 'customer_id' в датафрейме было не обнаружено. Количество пропусков - 11364 шт, при этом количество строк, содержащиие пропуски, равно 2208. Было выявлено, что пропущенные значения возникли в процессе объединения таблиц. Также выявлено не соотвествие типов данных столбцов 'total_charges', 'senior_citizen', 'begin_date', 'end_date'.
  1. Создан целевой признак 'leave' (факт ухода клиента от оператора связи) на основе признака "Дата окончания пользования услугами". Исследован и обнаружен дисбаланс классов (см. рисунок ниже), который был учтен в дальнейшем при обучении моделей машинного обучения
In [62]:
class_imbalance(df_before_delete, 'leave')
  1. Проведена обработка данных. Были заменены типы данных столбцов 'total_charges', 'senior_citizen', 'begin_date', 'end_date' на соответсвующие информации, представленной таблицами. Пропуски в датасете были обработаны путем замены на соответсвующие значения. В столбце 'multiple_lines' было заменно 682 пропуска путем замены на строковое значение 'No', в столбцах 'internet_service', 'online_security', 'online_backup', 'device_protection', 'tech_support', 'streaming_tv', 'streaming_movies' было заменно по 1526 пропусков, при этом в столбце 'internet_service' путем замены на строковое значение 'No internet', в остальных - на значение 'No'.
  1. Из известных данных созданы 2 синтетических признака6 'duration' и 'contract_end'. Признак 'duration' содержит информацию о длительности обслуживания клиента в днях, признак 'contract_end' - о близости окончания ежемесячного договора, при чем значение варьируется от 0 до 1, где значение около 0 - это договор только заключен, около 1 - договор подходит к концу и требуется пролонгация.
  1. Проведен анализ признаков по отдельности в разрезе целевой переменной. Распределение значений признаков представлено ниже.
  • Анализ признаков с категориальными значениями:
In [63]:
analysis_сat_report(df_before_delete, cat_col_list, cat_name_list)
  • Анализ признаков с количественными значениями:
In [65]:
analysis_num_report(df_before_delete, num_list)
monthly_charges :

total_charges :

duration :

contract_end :

Рассматривая распределения признаков в разрезе целевой переменной, можно отметить:

  • Наиболее часто встречаются клиенты с ежемесячными тратами на услуги до значения 20 и с общими тратам до значения 300, при этом из графика можно предположить, что клиенты с такими тратами в процентоном отношении реше уходят от оператора,
  • Наиболее частым способом выставления счета является электронная почта, типом договора - ежемесячный (примерно в три раза превышает остальные типы), способом оплаты - электронный чек, эти же значения среди всех остальных значений рассматриваемых признаков имеют большее влияние на отток клиентов от оператора,
  • Людей пенсионного возраста значительно меньше не пенсиннного (примерно в 5 раз), клиенты без иждивенцев в два раза превышают клиентов с иждивенцами, эти же значения оказывают сильнее влияние на целевой признак. Доля мужчин и женщин среди клиентов одинаковая, влияние на целевой признак не прослеживается,
  • Наиболее часто встречаеммой интернет-услугой является оптоволоконная связь, при этом отток клиентов у этой услуги больше, чем у DSL и у клиентов, которые не пользуются интренетом. Чаще всего клиенты не пользуются межсетевым экраном, облачным хранилищем файлов, антивирусом, выделенной линией технической поддержки,Онлайн-ТВ, Онлайн-кинотеатром, услугой подключения телефонного аппарата к нескольким линиям. Заметна тендеция более высокого оттока клиентов с наличием этих услуг,
  • Наибольший отток клиентов от оператора связи наблюдается при длительности пользования услугами в районе 1100 дней, при этом клиенты с длительностю договора выше 2000 практически не уходят от оператора, что вызвано их лояльностью. Клиенты, которые только заключили договор, тоже реже уходят от оператора;
  1. Проведен анализ корреляции признаков. Ниже представлена тепловая карта, построенная с помощью библиотеки phik:
In [66]:
plot_correlation_matrix(phik_overview.values, x_labels=phik_overview.columns, y_labels=phik_overview.index, 
                        vmin=0, vmax=1, color_map='Blues', title=r'correlation $\phi_K$', fontsize_factor=1.5,
                        figsize=(16,20))
plt.tight_layout()
  1. Проведенный анализ корреляции показал, что на целевой признак (отток клиентов) сильнее всего влияет: длительность обслуживания клиента (0,37), близость окончания договора (0,32) и количество потраченных денег (0,3). Меньше всего влияют на целевой признак: пол клиента (0,01), наличие иждивенцев (0,05) и пользование интернет-услугами (0,06). Также имеется высокая мультиколлинеарность между признаками 'internet_service' и 'monthly_charges'.
  1. На основе анализа распределения признаков, анализа их корреляции между собой и с целевым признаком, а также принципа избыточности удалены 6 признаков:
    • 'customer_id' - уникальные номер клиента,
    • 'begin_date' - дата начала пользования услугами,
    • 'end_date' - дата окончания пользования услугами,
    • 'gender' - пол,
    • 'internet_service' - наличие услуг Интернет,
    • 'Dependents' - наличие иждивенцев.

Итоговый датасет содержал 16 признаков и 7043 объекта, пропущенных значений нет.

Обучение моделей машинного обучения¶

  1. Итоговый датасет признаков был разделен на обучающие и тестовые выборки в соотношении 3 к 1. Составлен Pipiline для кодирования и масштабирования признаков, при чем OneHotEncoder используется для линейных моделей, OrdinalEncoder - для моделей на основе дерева решений. Для обучения были выбраны следующие модели:
  • LogisticRegression,
  • RandomForestClassifier,
  • LightGBM Classifier,
  • CatBoostClassifier.
  1. Обучение и оценка моделей производилась через перекрестную проверку с использованием 3 блоков для кросс-валидации. Mодели LightGBMClassifier и CatBoostClassifier оказались наиболее точными по метрике ROC-AUC со значениями 0.897 и 0.894 соответсвенно. Худший результат показала модель линейной регрессии - 0,79. RandomForestClassifier показала значение ROC-AUC, равной 0.88.
In [67]:
total
Out[67]:
ROC-AUC
LogisticRegression: 0.790
RandomForestClassifier: 0.875
LightGBMClassifier: 0.897
CatBoostClassifier: 0.894
  1. Лучшей моделью была выбрана модель градиентного бустинга LightGBMClassifier с гиперпараметрами:
  • random_state=140823,
  • class_weight='balanced',
  • num_leaves = 390,
  • n_estimators = 410,
  • max_depth = 2,
  • learning_rate = 0.16.

Гиперпараметры подбирались методом рандомизированного поиска по параметрам - RandomizedSearchCV с бюджетом вычислений n_iter, равным 100. Выбранное распределение по возможным значениям гиперпараметров представлено ниже:

  • 'learning_rate': np.arange(0.01, 0.21, 0.05),
  • 'num_leaves': np.arange(10, 510, 10),
  • 'max_depth': np.arange(1, 15, 1),
  • 'n_estimators': range (10, 1000, 10)

Проверка качества выбранной модели¶

  1. Метрика ROC-AUC выбранной модели (LGBMClassifier) на тестовой выборке составила 0.92, что выше условия задания - метрика ROC-AUC на тестовой выборке должна показать 0.85. Рассчитана доля правильных ответов Accuracy, равная 0.84. Дополнительно построена ROC-кривая для модели:
In [68]:
plt.figure(figsize=(8, 5))
sns.lineplot(x=fpr, y=tpr)

sns.lineplot(x=[0, 1], 
             y=[0, 1], 
             linestyle='--')\
.set(xlabel='Ложноположительный результат', 
     ylabel='Истинноположительный результат',  
     title = 'ROC-кривая')

plt.xlim(0,1) 
plt.ylim(0,1)
plt.legend(['Лучшая модель', 'Случайная модель'])
plt.show()

На графике по горизонтали показана доля ложноположительных ответов (False Positive Rate), по вертикали - доля истинно положительных ответов (True Positive Rate). Для модели, которая всегда отвечает случайно, ROC-кривая представлена прямой оранжевой пунктирной линией. Касательно качества модели на графике, если модель не делает ошибок, то кривая будет стремиться к точке (0.0,1.0), в противном случае, AUC-ROC стремиться к 0.5, то есть случайно выдавать вероятность классов. Полученная площадь под кривой метрика AUC-ROC в 0.92 говорит о том, что рассматриваемая модель хорошо предсказывает значения классов.

  1. Была построена матрица ошибок, наглядно представляющая результаты вычислений метрик точности и полноты. Матрица ошибок сформирована следующим образом: по диагонали от верхнего левого угла выстроены правильные прогнозы, вне главной диагонали — ошибочные варианты и содержит:
  • Истинно отрицательные ответы (True Negative) в левом верхнем углу,
  • Истинно положительные ответы (True Positive) в правом нижнем углу,
  • Ложноположительные ответы (False Positive) в правом верхнем углу,
  • Ложноотрицательные ответы (False Negative) в левом нижнем углу.
In [69]:
plt.figure(figsize=(6, 6))
sns.heatmap(cm, annot=True, linewidth=.5, fmt=".0f", cbar=False)
plt.title("Матрица ошибок", fontsize=20)
plt.xlabel('Предсказания', fontsize=14)
plt.ylabel('Истинный класс', fontsize=14)
plt.show()

Цель задачи состоит в максимизации истинно положительных и истинно отрицательных ответов и минимизации ложноположительных и ложноотрицательных ответов. Выбранная модель LightGBMClassifier выдает 1665 истинно положительных и 289 истинно отрицательных ответов, при этом на долю ложных приходится 297 положительных и 74 отрицательных ответов соответственно.

  1. Рассмотрена важность признаков.
In [70]:
plt.figure(figsize=(8, 4))
sns.barplot(feature_importance, y = feature_importance.index, x = 'Важность')
plt.title('Важность признаков', fontsize = 16)
plt.show()

Наибольшее влияние на прогнозирование модели оказывают синтетические признаки: близость окончания ежемесячного договора 'contract_end' (423) и длительность обслуживания клиента 'duration' (301). Также сильное влияение имеют признаки с количественными значениями: ежемесячные траты на услуги 'monthly_charges' (161) и потраченные деньги на услуги 'total_charges' (172). Наименьшее влияние оказывают услуги онлайн-ТВ (1) и межсетевого экрана (3).

Рекомендации о введении модели в эксплуатацию (или её переработки)¶

Выбранная модель машинного обучения LightGBMClassifier эффективно справляется с поставленой задачей предсказания ухода клиентов от оператора связи «Ниединогоразрыва.ком» со значением метрики ROC-AUC в 0.92, что выше поставленной задачи - "метрика ROC-AUC на тестовой выборке должна составлять не менее 0.85". Высокое качество предсказания модели получилось достичь за счет устранения мультиколлиарности, удаления избыточных признаков и признаков имеющих высокую корреляцию с целевым признаком. Для предотвращения утечки данных в модели используются конвейеры (pipeline) для кодирования и масштабирования признаков.

Так как модель позволяет предсказывать с высокой точностью уход клиентов, предоставляется возможность заказчику в соответсвии с предсказанием применять действия по удержанию клиентов, например, выдавать промо-купоны, скидки и прочие пакеты стимулирования.

Также модель дала возможность определить наиболее важные признаки: близость окончания ежемесячного договора, длительность обслуживания клиента, ежемесячные траты на услуги, потраченные деньги на услуги. Заказчику рекомендуется обратить внимание на эти признаки, влияя на которые можно снизить вероятность ухода клиентов.

Модель машинного обучения рекомендуется к вводу в экслуатацию.